diff options
219 files changed, 4705 insertions, 1965 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 834398e5c2c2..ac756ea1d624 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -1573,6 +1573,13 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +java_aconfig_library { + name: "power_flags_lib_host", + aconfig_declarations: "power_flags", + host_supported: true, + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} + // Content aconfig_declarations { name: "android.content.flags-aconfig", diff --git a/core/api/current.txt b/core/api/current.txt index 21929658cbb9..dd606774b770 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -1207,7 +1207,6 @@ package android { field public static final int minResizeHeight = 16843670; // 0x1010396 field public static final int minResizeWidth = 16843669; // 0x1010395 field public static final int minSdkVersion = 16843276; // 0x101020c - field @FlaggedApi("android.sdk.major_minor_versioning_scheme") public static final int minSdkVersionFull = 16844461; // 0x10106ad field public static final int minWidth = 16843071; // 0x101013f field public static final int minimumHorizontalAngle = 16843901; // 0x101047d field public static final int minimumVerticalAngle = 16843902; // 0x101047e diff --git a/core/java/android/content/pm/parsing/ApkLite.java b/core/java/android/content/pm/parsing/ApkLite.java index 1d8209da6559..b8cf70960ea3 100644 --- a/core/java/android/content/pm/parsing/ApkLite.java +++ b/core/java/android/content/pm/parsing/ApkLite.java @@ -18,6 +18,7 @@ package android.content.pm.parsing; import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.pm.ApplicationInfo; import android.content.pm.ArchivedPackageParcel; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -184,6 +185,11 @@ public class ApkLite { */ private final @Nullable ArchivedPackageParcel mArchivedPackage; + /** + * pageSizeCompat info from manifest file + */ + private final int mPageSizeCompat; + public ApkLite(String path, String packageName, String splitName, boolean isFeatureSplit, String configForSplit, String usesSplitName, boolean isSplitRequired, int versionCode, int versionCodeMajor, int revisionCode, int installLocation, @@ -200,7 +206,8 @@ public class ApkLite { List<String> usesStaticLibraries, long[] usesStaticLibrariesVersionsMajor, String[][] usesStaticLibrariesCertDigests, boolean updatableSystem, - String emergencyInstaller, List<SharedLibraryInfo> declaredLibraries) { + String emergencyInstaller, List<SharedLibraryInfo> declaredLibraries, + int pageSizeCompat) { mPath = path; mPackageName = packageName; mSplitName = splitName; @@ -245,6 +252,7 @@ public class ApkLite { mEmergencyInstaller = emergencyInstaller; mArchivedPackage = null; mDeclaredLibraries = declaredLibraries; + mPageSizeCompat = pageSizeCompat; } public ApkLite(String path, ArchivedPackageParcel archivedPackage) { @@ -292,6 +300,7 @@ public class ApkLite { mEmergencyInstaller = null; mArchivedPackage = archivedPackage; mDeclaredLibraries = null; + mPageSizeCompat = ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_UNDEFINED; } /** @@ -676,11 +685,19 @@ public class ApkLite { return mArchivedPackage; } + /** + * pageSizeCompat info from manifest file + */ + @DataClass.Generated.Member + public int getPageSizeCompat() { + return mPageSizeCompat; + } + @DataClass.Generated( - time = 1731589363302L, + time = 1738189581427L, codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/content/pm/parsing/ApkLite.java", - inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.Nullable java.lang.String mSplitName\nprivate final @android.annotation.Nullable java.lang.String mUsesSplitName\nprivate final @android.annotation.Nullable java.lang.String mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mSplitTypes\nprivate final int mVersionCodeMajor\nprivate final int mVersionCode\nprivate final int mRevisionCode\nprivate final int mInstallLocation\nprivate final int mMinSdkVersion\nprivate final int mTargetSdkVersion\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.NonNull android.content.pm.SigningDetails mSigningDetails\nprivate final boolean mFeatureSplit\nprivate final boolean mIsolatedSplits\nprivate final boolean mSplitRequired\nprivate final boolean mCoreApp\nprivate final boolean mDebuggable\nprivate final boolean mProfileableByShell\nprivate final boolean mMultiArch\nprivate final boolean mUse32bitAbi\nprivate final boolean mExtractNativeLibs\nprivate final boolean mUseEmbeddedDex\nprivate final @android.annotation.Nullable java.lang.String mTargetPackageName\nprivate final boolean mOverlayIsStatic\nprivate final int mOverlayPriority\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyName\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyValue\nprivate final int mRollbackDataPolicy\nprivate final boolean mHasDeviceAdminReceiver\nprivate final boolean mIsSdkLibrary\nprivate final boolean mIsStaticLibrary\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesSdkLibraries\nprivate final @android.annotation.Nullable long[] mUsesSdkLibrariesVersionsMajor\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesSdkLibrariesCertDigests\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesStaticLibraries\nprivate final @android.annotation.Nullable long[] mUsesStaticLibrariesVersions\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesStaticLibrariesCertDigests\nprivate final boolean mUpdatableSystem\nprivate final @android.annotation.Nullable java.lang.String mEmergencyInstaller\nprivate final @android.annotation.NonNull java.util.List<android.content.pm.SharedLibraryInfo> mDeclaredLibraries\nprivate final @android.annotation.Nullable android.content.pm.ArchivedPackageParcel mArchivedPackage\npublic long getLongVersionCode()\nprivate boolean hasAnyRequiredSplitTypes()\nclass ApkLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)") + inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.Nullable java.lang.String mSplitName\nprivate final @android.annotation.Nullable java.lang.String mUsesSplitName\nprivate final @android.annotation.Nullable java.lang.String mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mSplitTypes\nprivate final int mVersionCodeMajor\nprivate final int mVersionCode\nprivate final int mRevisionCode\nprivate final int mInstallLocation\nprivate final int mMinSdkVersion\nprivate final int mTargetSdkVersion\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.NonNull android.content.pm.SigningDetails mSigningDetails\nprivate final boolean mFeatureSplit\nprivate final boolean mIsolatedSplits\nprivate final boolean mSplitRequired\nprivate final boolean mCoreApp\nprivate final boolean mDebuggable\nprivate final boolean mProfileableByShell\nprivate final boolean mMultiArch\nprivate final boolean mUse32bitAbi\nprivate final boolean mExtractNativeLibs\nprivate final boolean mUseEmbeddedDex\nprivate final @android.annotation.Nullable java.lang.String mTargetPackageName\nprivate final boolean mOverlayIsStatic\nprivate final int mOverlayPriority\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyName\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyValue\nprivate final int mRollbackDataPolicy\nprivate final boolean mHasDeviceAdminReceiver\nprivate final boolean mIsSdkLibrary\nprivate final boolean mIsStaticLibrary\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesSdkLibraries\nprivate final @android.annotation.Nullable long[] mUsesSdkLibrariesVersionsMajor\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesSdkLibrariesCertDigests\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesStaticLibraries\nprivate final @android.annotation.Nullable long[] mUsesStaticLibrariesVersions\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesStaticLibrariesCertDigests\nprivate final boolean mUpdatableSystem\nprivate final @android.annotation.Nullable java.lang.String mEmergencyInstaller\nprivate final @android.annotation.NonNull java.util.List<android.content.pm.SharedLibraryInfo> mDeclaredLibraries\nprivate final @android.annotation.Nullable android.content.pm.ArchivedPackageParcel mArchivedPackage\nprivate final int mPageSizeCompat\npublic long getLongVersionCode()\nprivate boolean hasAnyRequiredSplitTypes()\nclass ApkLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)") @Deprecated private void __metadata() {} diff --git a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java index 71d0a04760ac..26252a990676 100644 --- a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java +++ b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java @@ -22,6 +22,7 @@ import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER; import android.annotation.NonNull; import android.app.admin.DeviceAdminReceiver; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.SharedLibraryInfo; @@ -459,6 +460,7 @@ public class ApkLiteParseUtils { boolean overlayIsStatic = false; int overlayPriority = 0; int rollbackDataPolicy = 0; + int pageSizeCompat = ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_UNDEFINED; String requiredSystemPropertyName = null; String requiredSystemPropertyValue = null; @@ -516,6 +518,10 @@ public class ApkLiteParseUtils { boolean hasBindDeviceAdminPermission = android.Manifest.permission.BIND_DEVICE_ADMIN.equals(permission); + pageSizeCompat = parser.getAttributeIntValue(ANDROID_RES_NAMESPACE, + "pageSizeCompat", + ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_UNDEFINED); + final int innerDepth = parser.getDepth(); int innerType; while ((innerType = parser.next()) != XmlPullParser.END_DOCUMENT @@ -817,7 +823,7 @@ public class ApkLiteParseUtils { usesSdkLibrariesVersionsMajor, usesSdkLibrariesCertDigests, isStaticLibrary, usesStaticLibraries, usesStaticLibrariesVersions, usesStaticLibrariesCertDigests, updatableSystem, emergencyInstaller, - declaredLibraries)); + declaredLibraries, pageSizeCompat)); } private static ParseResult<String[]> parseAdditionalCertificates(ParseInput input, diff --git a/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java b/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java index d2d3a6840acc..c7403c0ea98c 100644 --- a/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java +++ b/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java @@ -339,36 +339,6 @@ public class FrameworkParsingPackageUtils { } /** - * Check if a package is compatible with this platform with regards to its - * its minSdkVersionFull. - * - * @param minSdkVersionFullString A string representation of a major.minor version, - * e.g. "12.34" - * @param platformMinSdkVersionFull The major and minor version of the platform, i.e. the value - * of Build.VERSION.SDK_INT_FULL - * @param input A ParseInput object to report success or failure - */ - public static ParseResult<Void> verifyMinSdkVersionFull(@NonNull String minSdkVersionFullString, - int platformMinSdkVersionFull, @NonNull ParseInput input) { - int minSdkVersionFull; - try { - minSdkVersionFull = Build.parseFullVersion(minSdkVersionFullString); - } catch (IllegalStateException e) { - return input.error(PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED, - e.getMessage()); - } - if (minSdkVersionFull <= platformMinSdkVersionFull) { - return input.success(null); - } - return input.error(PackageManager.INSTALL_FAILED_OLDER_SDK, - "Requires newer sdk version " - + Build.fullVersionToString(minSdkVersionFull) - + " (current version is " - + Build.fullVersionToString(platformMinSdkVersionFull) - + ")"); - } - - /** * Computes the targetSdkVersion to use at runtime. If the package is not compatible with this * platform, populates {@code outError[0]} with an error message. * <p> diff --git a/core/java/android/content/pm/parsing/PackageLite.java b/core/java/android/content/pm/parsing/PackageLite.java index 0e11eecfc7ec..43a3645f34cd 100644 --- a/core/java/android/content/pm/parsing/PackageLite.java +++ b/core/java/android/content/pm/parsing/PackageLite.java @@ -138,6 +138,11 @@ public class PackageLite { */ private final @Nullable ArchivedPackageParcel mArchivedPackage; + /** + * pageSizeCompat info from manifest file + */ + private final int mPageSizeCompat; + public PackageLite(String path, String baseApkPath, ApkLite baseApk, String[] splitNames, boolean[] isFeatureSplits, String[] usesSplitNames, String[] configForSplit, String[] splitApkPaths, int[] splitRevisionCodes, @@ -182,6 +187,7 @@ public class PackageLite { mTargetSdk = targetSdk; mDeclaredLibraries = baseApk.getDeclaredLibraries(); mArchivedPackage = baseApk.getArchivedPackage(); + mPageSizeCompat = baseApk.getPageSizeCompat(); } /** @@ -511,11 +517,19 @@ public class PackageLite { return mArchivedPackage; } + /** + * pageSizeCompat info from manifest file + */ + @DataClass.Generated.Member + public int getPageSizeCompat() { + return mPageSizeCompat; + } + @DataClass.Generated( - time = 1731591578587L, + time = 1738193799106L, codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/content/pm/parsing/PackageLite.java", - inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.NonNull java.lang.String mBaseApkPath\nprivate final @android.annotation.Nullable java.lang.String[] mSplitApkPaths\nprivate final @android.annotation.Nullable java.lang.String[] mSplitNames\nprivate final @android.annotation.Nullable java.lang.String[] mUsesSplitNames\nprivate final @android.annotation.Nullable java.lang.String[] mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mBaseRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String>[] mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String>[] mSplitTypes\nprivate final int mVersionCodeMajor\nprivate final int mVersionCode\nprivate final int mTargetSdk\nprivate final int mBaseRevisionCode\nprivate final @android.annotation.Nullable int[] mSplitRevisionCodes\nprivate final int mInstallLocation\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.NonNull android.content.pm.SigningDetails mSigningDetails\nprivate final @android.annotation.Nullable boolean[] mIsFeatureSplits\nprivate final boolean mIsolatedSplits\nprivate final boolean mSplitRequired\nprivate final boolean mCoreApp\nprivate final boolean mDebuggable\nprivate final boolean mMultiArch\nprivate final boolean mUse32bitAbi\nprivate final boolean mExtractNativeLibs\nprivate final boolean mProfileableByShell\nprivate final boolean mUseEmbeddedDex\nprivate final boolean mIsSdkLibrary\nprivate final boolean mIsStaticLibrary\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesSdkLibraries\nprivate final @android.annotation.Nullable long[] mUsesSdkLibrariesVersionsMajor\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesSdkLibrariesCertDigests\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesStaticLibraries\nprivate final @android.annotation.Nullable long[] mUsesStaticLibrariesVersions\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesStaticLibrariesCertDigests\nprivate final @android.annotation.NonNull java.util.List<android.content.pm.SharedLibraryInfo> mDeclaredLibraries\nprivate final @android.annotation.Nullable android.content.pm.ArchivedPackageParcel mArchivedPackage\npublic java.util.List<java.lang.String> getAllApkPaths()\npublic long getLongVersionCode()\nprivate boolean hasAnyRequiredSplitTypes()\nclass PackageLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)") + inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.NonNull java.lang.String mBaseApkPath\nprivate final @android.annotation.Nullable java.lang.String[] mSplitApkPaths\nprivate final @android.annotation.Nullable java.lang.String[] mSplitNames\nprivate final @android.annotation.Nullable java.lang.String[] mUsesSplitNames\nprivate final @android.annotation.Nullable java.lang.String[] mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mBaseRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String>[] mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String>[] mSplitTypes\nprivate final int mVersionCodeMajor\nprivate final int mVersionCode\nprivate final int mTargetSdk\nprivate final int mBaseRevisionCode\nprivate final @android.annotation.Nullable int[] mSplitRevisionCodes\nprivate final int mInstallLocation\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.NonNull android.content.pm.SigningDetails mSigningDetails\nprivate final @android.annotation.Nullable boolean[] mIsFeatureSplits\nprivate final boolean mIsolatedSplits\nprivate final boolean mSplitRequired\nprivate final boolean mCoreApp\nprivate final boolean mDebuggable\nprivate final boolean mMultiArch\nprivate final boolean mUse32bitAbi\nprivate final boolean mExtractNativeLibs\nprivate final boolean mProfileableByShell\nprivate final boolean mUseEmbeddedDex\nprivate final boolean mIsSdkLibrary\nprivate final boolean mIsStaticLibrary\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesSdkLibraries\nprivate final @android.annotation.Nullable long[] mUsesSdkLibrariesVersionsMajor\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesSdkLibrariesCertDigests\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesStaticLibraries\nprivate final @android.annotation.Nullable long[] mUsesStaticLibrariesVersions\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesStaticLibrariesCertDigests\nprivate final @android.annotation.NonNull java.util.List<android.content.pm.SharedLibraryInfo> mDeclaredLibraries\nprivate final @android.annotation.Nullable android.content.pm.ArchivedPackageParcel mArchivedPackage\nprivate final int mPageSizeCompat\npublic java.util.List<java.lang.String> getAllApkPaths()\npublic long getLongVersionCode()\nprivate boolean hasAnyRequiredSplitTypes()\nclass PackageLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)") @Deprecated private void __metadata() {} diff --git a/core/java/android/hardware/biometrics/BiometricConstants.java b/core/java/android/hardware/biometrics/BiometricConstants.java index 7dc6afba3f1c..a7fbce51e9df 100644 --- a/core/java/android/hardware/biometrics/BiometricConstants.java +++ b/core/java/android/hardware/biometrics/BiometricConstants.java @@ -188,6 +188,24 @@ public interface BiometricConstants { int BIOMETRIC_ERROR_CONTENT_VIEW_MORE_OPTIONS_BUTTON = 22; /** + * The error code returned after lock out error happens, the error dialog shows, and the users + * dismisses the dialog. This is a placeholder that is currently only used by the support + * library. + * + * @hide + */ + int BIOMETRIC_ERROR_LOCKOUT_ERROR_DIALOG_DISMISSED = 23; + + /** + * The error code returned after biometric hardware error happens, the error dialog shows, and + * the users dismisses the dialog.This is a placeholder that is currently only used by the + * support library. + * + * @hide + */ + int BIOMETRIC_ERROR_BIOMETRIC_HARDWARE_ERROR_DIALOG_DISMISSED = 24; + + /** * This constant is only used by SystemUI. It notifies SystemUI that authentication was paused * because the authentication attempt was unsuccessful. * @hide @@ -219,6 +237,8 @@ public interface BiometricConstants { BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE, BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS, BIOMETRIC_ERROR_CONTENT_VIEW_MORE_OPTIONS_BUTTON, + BIOMETRIC_ERROR_LOCKOUT_ERROR_DIALOG_DISMISSED, + BIOMETRIC_ERROR_BIOMETRIC_HARDWARE_ERROR_DIALOG_DISMISSED, BIOMETRIC_PAUSED_REJECTED}) @Retention(RetentionPolicy.SOURCE) @interface Errors {} diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java index 89a6b02b56c4..56d272768a66 100644 --- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java @@ -1616,7 +1616,13 @@ public class CameraDeviceImpl extends CameraDevice // request if no repeating request is active. A default capture request is created here // for initial use. The capture callback will provide capture results that include the // actual capture parameters used for the streaming. - CaptureRequest.Builder builder = createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); + CameraMetadataNative templatedRequest = mRemoteDevice.createDefaultRequest( + CameraDevice.TEMPLATE_PREVIEW); + + CaptureRequest.Builder builder = new CaptureRequest.Builder( + templatedRequest, /*reprocess*/false, CameraCaptureSession.SESSION_ID_NONE, + getId(), /*physicalCameraIdSet*/ null); + for (Surface surface : surfaces) { builder.addTarget(surface); } diff --git a/core/java/android/hardware/display/DisplayTopology.java b/core/java/android/hardware/display/DisplayTopology.java index 555ff4b271fd..785a0e0adc48 100644 --- a/core/java/android/hardware/display/DisplayTopology.java +++ b/core/java/android/hardware/display/DisplayTopology.java @@ -750,7 +750,7 @@ public final class DisplayTopology implements Parcelable { new SparseArray<>(); for (int id : displayIds) { if (densityPerDisplay.get(id) == 0) { - Slog.w(TAG, "Cannot construct graph, no density for display " + id); + Slog.e(TAG, "Cannot construct graph, no density for display " + id); return null; } adjacentDisplaysPerId.append(id, new ArrayList<>(Math.min(10, displayIds.size()))); diff --git a/core/java/android/os/ITradeInMode.aidl b/core/java/android/os/ITradeInMode.aidl index f15954d14d0e..d05f52cf6a90 100644 --- a/core/java/android/os/ITradeInMode.aidl +++ b/core/java/android/os/ITradeInMode.aidl @@ -59,4 +59,37 @@ interface ITradeInMode { * ENTER_TRADE_IN_MODE permission is required. */ boolean enterEvaluationMode(); + + /** + * Schedules a wipe to trigger SUW for trade-in mode testing. A reboot is + * required. After this, startTesting() can be called. + * + * ENTER_TRADE_IN_MODE permission is required and ro.debuggable must be 1. + */ + void scheduleWipeForTesting(); + + /** + * Enables testing. This only takes effect after the next reboot, and is + * only allowed in ro.debuggable builds. On the following boot, normal + * adbd will be disabled and trade-in mode adbd will be enabled instead. + * + * ENTER_TRADE_IN_MODE permission is required and ro.debuggable must be 1. + */ + void startTesting(); + + /** + * Disables testing. This disables trade-in mode and removes any scheduled + * trade-in mode wipe. + * + * ENTER_TRADE_IN_MODE permission is required, ro.debuggable must be 1, and + * startTesting() must have been called. + */ + void stopTesting(); + + /** + * Returns whether the device is testing trade-in mode. + * + * ENTER_TRADE_IN_MODE permission is required and ro.debuggable must be 1. + */ + boolean isTesting(); } diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java index e769abec7dd9..5129af6be442 100644 --- a/core/java/android/os/PowerManager.java +++ b/core/java/android/os/PowerManager.java @@ -4213,6 +4213,17 @@ public final class PowerManager { else mFlags &= ~UNIMPORTANT_FOR_LOGGING; } + /** @hide */ + public void updateUids(int[] uids) { + synchronized (mToken) { + try { + mService.updateWakeLockUids(mToken, uids); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + @Override public String toString() { synchronized (mToken) { diff --git a/core/java/android/os/TestLooperManager.java b/core/java/android/os/TestLooperManager.java index 82bdb2280c35..2d9d025b8d80 100644 --- a/core/java/android/os/TestLooperManager.java +++ b/core/java/android/os/TestLooperManager.java @@ -174,6 +174,7 @@ public class TestLooperManager { try { execution.wait(); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } if (execution.response != null) { throw new RuntimeException(execution.response); @@ -231,6 +232,7 @@ public class TestLooperManager { try { mLooperHolderLatch.await(); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } } @@ -245,6 +247,7 @@ public class TestLooperManager { processMessage(take); } } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } } } diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java index 311fbee2fc4b..e361ac7d2a5e 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -594,6 +594,7 @@ public final class SurfaceControl implements Parcelable { private final Runnable mFreeNativeResources; private boolean mRemoved = false; + private OnJankDataListener mListener; private OnJankDataListenerRegistration() { mNativeObject = 0; @@ -604,6 +605,8 @@ public final class SurfaceControl implements Parcelable { mNativeObject = nativeCreateJankDataListenerWrapper(surface.mNativeObject, listener); mFreeNativeResources = (mNativeObject == 0) ? () -> {} : sRegistry.registerNativeAllocation(this, mNativeObject); + // Make sure the listener doesn't get GCed as long as the registration is alive. + mListener = listener; } /** @@ -643,6 +646,7 @@ public final class SurfaceControl implements Parcelable { if (!mRemoved) { removeAfter(0); } + mListener = null; mFreeNativeResources.run(); } } diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index ec0d9152468e..0f5476f58f74 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -10082,6 +10082,7 @@ public class RemoteViews implements Parcelable, Filter { if (mApplication != null) { // mApplication may be null if this was created with DrawInstructions constructor. out.write(RemoteViewsProto.PACKAGE_NAME, mApplication.packageName); + out.write(RemoteViewsProto.UID, mApplication.uid); } Resources appResources = getContextForResourcesEnsuringCorrectCachedApkPaths( context).getResources(); @@ -10163,6 +10164,7 @@ public class RemoteViews implements Parcelable, Filter { int mApplyFlags = 0; long mProviderInstanceId = -1; String mPackageName = null; + Integer mUid = null; SizeF mIdealSize = null; String mLayoutResName = null; String mLightBackgroundResName = null; @@ -10185,6 +10187,9 @@ public class RemoteViews implements Parcelable, Filter { case (int) RemoteViewsProto.PACKAGE_NAME: ref.mPackageName = in.readString(RemoteViewsProto.PACKAGE_NAME); break; + case (int) RemoteViewsProto.UID: + ref.mUid = in.readInt(RemoteViewsProto.UID); + break; case (int) RemoteViewsProto.IDEAL_SIZE: final long idealSizeToken = in.start(RemoteViewsProto.IDEAL_SIZE); ref.mIdealSize = createSizeFFromProto(in); @@ -10286,8 +10291,9 @@ public class RemoteViews implements Parcelable, Filter { Resources appResources = null; if (!ref.mHasDrawInstructions) { checkProtoResultNotNull(ref.mPackageName, "No application info"); - rv.mApplication = context.getPackageManager().getApplicationInfo(ref.mPackageName, - /* flags= */ 0); + checkProtoResultNotNull(ref.mUid, "No uid"); + rv.mApplication = context.getPackageManager().getApplicationInfoAsUser( + ref.mPackageName, /* flags= */ 0, UserHandle.getUserId(ref.mUid)); appContext = rv.getContextForResourcesEnsuringCorrectCachedApkPaths(context); appResources = appContext.getResources(); diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java index 082bf5dc5a1c..c141bbaf087c 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -44,6 +44,7 @@ public enum DesktopModeFlags { // All desktop mode related flags to be overridden by developer option toggle will be added here // go/keep-sorted start DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE(Flags::disableNonResizableAppSnapResizing, true), + ENABLE_ACCESSIBLE_CUSTOM_HEADERS(Flags::enableAccessibleCustomHeaders, true), ENABLE_APP_HEADER_WITH_TASK_DENSITY(Flags::enableAppHeaderWithTaskDensity, true), ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION(Flags::enableCaptionCompatInsetForceConsumption, true), @@ -79,6 +80,7 @@ public enum DesktopModeFlags { Flags::enableDesktopWindowingMultiInstanceFeatures, true), ENABLE_DESKTOP_WINDOWING_PERSISTENCE(Flags::enableDesktopWindowingPersistence, true), ENABLE_DESKTOP_WINDOWING_QUICK_SWITCH(Flags::enableDesktopWindowingQuickSwitch, true), + ENABLE_DESKTOP_WINDOWING_SCVH_CACHE(Flags::enableDesktopWindowingScvhCacheBugFix, true), ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS(Flags::enableDesktopWindowingSizeConstraints, true), ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS(Flags::enableDesktopWindowingTaskbarRunningApps, true), @@ -89,6 +91,8 @@ public enum DesktopModeFlags { ENABLE_HOLD_TO_DRAG_APP_HANDLE(Flags::enableHoldToDragAppHandle, true), ENABLE_MINIMIZE_BUTTON(Flags::enableMinimizeButton, true), ENABLE_RESIZING_METRICS(Flags::enableResizingMetrics, true), + ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE( + Flags::enableRestoreToPreviousSizeFromDesktopImmersive, true), ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS(Flags::enableTaskResizingKeyboardShortcuts, true), ENABLE_TASK_STACK_OBSERVER_IN_SHELL(Flags::enableTaskStackObserverInShell, true), ENABLE_THEMED_APP_HEADERS(Flags::enableThemedAppHeaders, true), diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index ed22ec73aac8..6634ee0e1020 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -631,3 +631,13 @@ flag { description: "Enables full support of presentation API for connected displays." bug: "378503083" } + +flag { + name: "enable_full_screen_window_on_removing_split_screen_stage_bugfix" + namespace: "lse_desktop_experience" + description: "Enables clearing the windowing mode of a freeform window when removing the task from the split screen stage." + bug: "372791604" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig index 1b946afd506c..6f8852daae5f 100644 --- a/core/java/android/window/flags/windowing_sdk.aconfig +++ b/core/java/android/window/flags/windowing_sdk.aconfig @@ -185,3 +185,14 @@ flag { description: "Enables letterboxing for a safe region" bug: "380132497" } + +flag { + namespace: "windowing_sdk" + name: "fix_layout_existing_task" + description: "Layout the existing task to ensure the bounds are updated." + bug: "390291971" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +}
\ No newline at end of file diff --git a/core/java/com/android/internal/content/NativeLibraryHelper.java b/core/java/com/android/internal/content/NativeLibraryHelper.java index e170d6652863..8c64750b66e4 100644 --- a/core/java/com/android/internal/content/NativeLibraryHelper.java +++ b/core/java/com/android/internal/content/NativeLibraryHelper.java @@ -85,6 +85,8 @@ public class NativeLibraryHelper { final boolean extractNativeLibs; final boolean debuggable; + final boolean pageSizeCompatDisabled; + public static Handle create(File packageFile) throws IOException { final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing(); final ParseResult<PackageLite> ret = ApkLiteParseUtils.parsePackageLite(input.reset(), @@ -97,12 +99,15 @@ public class NativeLibraryHelper { } public static Handle create(PackageLite lite) throws IOException { + boolean isPageSizeCompatDisabled = lite.getPageSizeCompat() + == ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_MANIFEST_OVERRIDE_DISABLED; return create(lite.getAllApkPaths(), lite.isMultiArch(), lite.isExtractNativeLibs(), - lite.isDebuggable()); + lite.isDebuggable(), isPageSizeCompatDisabled); } public static Handle create(List<String> codePaths, boolean multiArch, - boolean extractNativeLibs, boolean debuggable) throws IOException { + boolean extractNativeLibs, boolean debuggable, boolean isPageSizeCompatDisabled) + throws IOException { final int size = codePaths.size(); final String[] apkPaths = new String[size]; final long[] apkHandles = new long[size]; @@ -119,7 +124,8 @@ public class NativeLibraryHelper { } } - return new Handle(apkPaths, apkHandles, multiArch, extractNativeLibs, debuggable); + return new Handle(apkPaths, apkHandles, multiArch, extractNativeLibs, debuggable, + isPageSizeCompatDisabled); } public static Handle createFd(PackageLite lite, FileDescriptor fd) throws IOException { @@ -130,17 +136,21 @@ public class NativeLibraryHelper { throw new IOException("Unable to open APK " + path + " from fd " + fd); } + boolean isPageSizeCompatDisabled = lite.getPageSizeCompat() + == ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_MANIFEST_OVERRIDE_DISABLED; + return new Handle(new String[]{path}, apkHandles, lite.isMultiArch(), - lite.isExtractNativeLibs(), lite.isDebuggable()); + lite.isExtractNativeLibs(), lite.isDebuggable(), isPageSizeCompatDisabled); } Handle(String[] apkPaths, long[] apkHandles, boolean multiArch, - boolean extractNativeLibs, boolean debuggable) { + boolean extractNativeLibs, boolean debuggable, boolean isPageSizeCompatDisabled) { this.apkPaths = apkPaths; this.apkHandles = apkHandles; this.multiArch = multiArch; this.extractNativeLibs = extractNativeLibs; this.debuggable = debuggable; + this.pageSizeCompatDisabled = isPageSizeCompatDisabled; mGuard.open("close"); } @@ -175,7 +185,8 @@ public class NativeLibraryHelper { private static native long nativeSumNativeBinaries(long handle, String cpuAbi); private native static int nativeCopyNativeBinaries(long handle, String sharedLibraryPath, - String abiToCopy, boolean extractNativeLibs, boolean debuggable); + String abiToCopy, boolean extractNativeLibs, boolean debuggable, + boolean pageSizeCompatDisabled); private static native int nativeCheckAlignment( long handle, @@ -203,7 +214,7 @@ public class NativeLibraryHelper { public static int copyNativeBinaries(Handle handle, File sharedLibraryDir, String abi) { for (long apkHandle : handle.apkHandles) { int res = nativeCopyNativeBinaries(apkHandle, sharedLibraryDir.getPath(), abi, - handle.extractNativeLibs, handle.debuggable); + handle.extractNativeLibs, handle.debuggable, handle.pageSizeCompatDisabled); if (res != INSTALL_SUCCEEDED) { return res; } diff --git a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java index 5c08dc6be1a0..db60e12e50b1 100644 --- a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java +++ b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java @@ -29,7 +29,6 @@ import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_UNEXPECTED_ import static android.os.Build.VERSION_CODES.DONUT; import static android.os.Build.VERSION_CODES.O; import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER; -import static android.sdk.Flags.majorMinorVersioningScheme; import static com.android.internal.pm.pkg.parsing.ParsingUtils.parseKnownActivityEmbeddingCerts; @@ -1690,21 +1689,6 @@ public class ParsingPackageUtils { targetCode = minCode; } - if (majorMinorVersioningScheme()) { - val = sa.peekValue(R.styleable.AndroidManifestUsesSdk_minSdkVersionFull); - if (val != null) { - if (val.type == TypedValue.TYPE_STRING && val.string != null) { - String minSdkVersionFullString = val.string.toString(); - ParseResult<Void> minSdkVersionFullResult = - FrameworkParsingPackageUtils.verifyMinSdkVersionFull( - minSdkVersionFullString, Build.VERSION.SDK_INT_FULL, input); - if (minSdkVersionFullResult.isError()) { - return input.error(minSdkVersionFullResult); - } - } - } - } - if (isApkInApex) { val = sa.peekValue(R.styleable.AndroidManifestUsesSdk_maxSdkVersion); if (val != null) { diff --git a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp index e78c5247d8a7..06fd80e37669 100644 --- a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp +++ b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp @@ -273,6 +273,7 @@ static install_status_t copyFileIfChanged(JNIEnv* env, void* arg, ZipFileRO* zip jboolean extractNativeLibs = *(jboolean*)args[1]; jboolean debuggable = *(jboolean*)args[2]; jboolean app_compat_16kb = *(jboolean*)args[3]; + jboolean pageSizeCompatDisabled = *(jboolean*)args[4]; install_status_t ret = INSTALL_SUCCEEDED; ScopedUtfChars nativeLibPath(env, *javaNativeLibPath); @@ -304,6 +305,16 @@ static install_status_t copyFileIfChanged(JNIEnv* env, void* arg, ZipFileRO* zip } if (offset % kPageSize != 0) { + // If page size app compat was disabled explicitly in manifest, don't extract libs on + // 16 KB page size device. + if (kPageSize == 0x4000 && pageSizeCompatDisabled) { + ALOGE("pageSizeCompat=disabled library '%s' is not PAGE(%zu)-" + "aligned within apk (APK alignment, not ELF alignment) -" + "and will not be extracted.\n", + fileName, kPageSize); + return INSTALL_FAILED_INVALID_APK; + } + // If the library is zip-aligned correctly for 4kb devices and app compat is // enabled, on 16kb devices fallback to extraction if (offset % 0x1000 == 0 && app_compat_16kb) { @@ -537,13 +548,12 @@ static inline bool app_compat_16kb_enabled() { return !android::base::GetBoolProperty("pm.16kb.app_compat.disabled", false); } -static jint -com_android_internal_content_NativeLibraryHelper_copyNativeBinaries(JNIEnv *env, jclass clazz, - jlong apkHandle, jstring javaNativeLibPath, jstring javaCpuAbi, - jboolean extractNativeLibs, jboolean debuggable) -{ +static jint com_android_internal_content_NativeLibraryHelper_copyNativeBinaries( + JNIEnv* env, jclass clazz, jlong apkHandle, jstring javaNativeLibPath, jstring javaCpuAbi, + jboolean extractNativeLibs, jboolean debuggable, jboolean pageSizeCompatDisabled) { jboolean app_compat_16kb = app_compat_16kb_enabled(); - void* args[] = { &javaNativeLibPath, &extractNativeLibs, &debuggable, &app_compat_16kb }; + void* args[] = {&javaNativeLibPath, &extractNativeLibs, &debuggable, &app_compat_16kb, + &pageSizeCompatDisabled}; return (jint) iterateOverNativeFiles(env, apkHandle, javaCpuAbi, copyFileIfChanged, reinterpret_cast<void*>(args)); } @@ -804,7 +814,7 @@ static const JNINativeMethod gMethods[] = { {"nativeOpenApkFd", "(Ljava/io/FileDescriptor;Ljava/lang/String;)J", (void*)com_android_internal_content_NativeLibraryHelper_openApkFd}, {"nativeClose", "(J)V", (void*)com_android_internal_content_NativeLibraryHelper_close}, - {"nativeCopyNativeBinaries", "(JLjava/lang/String;Ljava/lang/String;ZZ)I", + {"nativeCopyNativeBinaries", "(JLjava/lang/String;Ljava/lang/String;ZZZ)I", (void*)com_android_internal_content_NativeLibraryHelper_copyNativeBinaries}, {"nativeSumNativeBinaries", "(JLjava/lang/String;)J", (void*)com_android_internal_content_NativeLibraryHelper_sumNativeBinaries}, diff --git a/core/proto/android/widget/remoteviews.proto b/core/proto/android/widget/remoteviews.proto index 6a987a475711..91dbf7b54534 100644 --- a/core/proto/android/widget/remoteviews.proto +++ b/core/proto/android/widget/remoteviews.proto @@ -57,6 +57,7 @@ message RemoteViewsProto { repeated bytes bitmap_cache = 14; optional RemoteCollectionCache remote_collection_cache = 15; repeated Action actions = 16; + optional int32 uid = 17; message RemoteCollectionCache { message Entry { diff --git a/core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml b/core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml index dd70087f7785..bf70a5eff47e 100644 --- a/core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml +++ b/core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml @@ -49,7 +49,7 @@ android:importantForAccessibility="no" /> <ViewStub - android:layout="@layout/conversation_face_pile_layout" + android:layout="@layout/notification_2025_conversation_face_pile_layout" android:layout_gravity="center_vertical|start" android:layout_width="@dimen/conversation_compact_face_pile_size" android:layout_height="@dimen/conversation_compact_face_pile_size" diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index 8c6fd1dfc47e..3edc5c108083 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -2572,10 +2572,6 @@ against a development branch, in which case it will only work against the development builds. --> <attr name="minSdkVersion" format="integer|string" /> - <!-- This is the minimum SDK major and minor version (e.g. "36.1") that - the application requires. Verified independently of minSdkVersion. - @FlaggedApi(android.sdk.Flags.FLAG_MAJOR_MINOR_VERSIONING_SCHEME) --> - <attr name="minSdkVersionFull" format="string" /> <!-- This is the SDK version number that the application is targeting. It is able to run on older versions (down to minSdkVersion), but was explicitly tested to work with the version specified here. diff --git a/core/res/res/values/public-final.xml b/core/res/res/values/public-final.xml index d8e89318a134..af1e5123096d 100644 --- a/core/res/res/values/public-final.xml +++ b/core/res/res/values/public-final.xml @@ -3953,8 +3953,7 @@ <public name="pageSizeCompat" /> <!-- @FlaggedApi(android.nfc.Flags.FLAG_NFC_ASSOCIATED_ROLE_SERVICES) --> <public name="wantsRoleHolderPriority"/> - <!-- @FlaggedApi(android.sdk.Flags.FLAG_MAJOR_MINOR_VERSIONING_SCHEME) --> - <public name="minSdkVersionFull"/> + <public name="removed_"/> <public name="removed_" /> <public name="removed_" /> <public name="removed_" /> @@ -3980,8 +3979,6 @@ <public type="attr" name="pageSizeCompat" id="0x010106ab" /> <!-- @FlaggedApi(android.nfc.Flags.FLAG_NFC_ASSOCIATED_ROLE_SERVICES) --> <public type="attr" name="wantsRoleHolderPriority" id="0x010106ac" /> - <!-- @FlaggedApi(android.sdk.Flags.FLAG_MAJOR_MINOR_VERSIONING_SCHEME) --> - <public type="attr" name="minSdkVersionFull" id="0x010106ad" /> <staging-public-group-final type="string" first-id="0x01b40000"> <!-- @FlaggedApi(android.content.pm.Flags.FLAG_SDK_DEPENDENCY_INSTALLER) diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 1edbffa9d572..1de8664231d7 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -259,6 +259,7 @@ applications that come with the platform <permission name="android.permission.MODIFY_DAY_NIGHT_MODE"/> <permission name="android.permission.ACCESS_LOWPAN_STATE"/> <permission name="android.permission.BACKUP"/> + <permission name="android.permission.ENTER_TRADE_IN_MODE"/> <!-- Needed for GMSCore Location API test only --> <permission name="android.permission.LOCATION_BYPASS"/> <!-- Needed for test only --> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 9e2d23b41556..404bbd1d0a33 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -270,6 +270,8 @@ <dimen name="bubble_bar_expanded_view_switch_offset">48dp</dimen> <!-- Minimum width of the bubble bar manage menu. --> <dimen name="bubble_bar_manage_menu_min_width">200dp</dimen> + <!-- The Bubble Bar drop zone square size. --> + <dimen name="bubble_bar_drop_zone_side_size">200dp</dimen> <!-- Size of the dismiss icon in the bubble bar manage menu. --> <dimen name="bubble_bar_manage_menu_dismiss_icon_size">16dp</dimen> <!-- Padding of the bubble bar manage menu, provides space for menu shadows --> diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java index da62be7f142f..2586bd6d86cb 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java @@ -189,7 +189,7 @@ public class DesktopModeStatus { * there should be no pooling. */ public static int getWindowDecorScvhPoolSize(@NonNull Context context) { - if (!Flags.enableDesktopWindowingScvhCacheBugFix()) return 0; + if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_SCVH_CACHE.isTrue()) return 0; final int maxTaskLimit = getMaxTaskLimit(context); if (maxTaskLimit > 0) { return maxTaskLimit; @@ -208,8 +208,7 @@ public class DesktopModeStatus { /** * Return {@code true} if the current device supports desktop mode. */ - @VisibleForTesting - public static boolean isDesktopModeSupported(@NonNull Context context) { + private static boolean isDesktopModeSupported(@NonNull Context context) { return context.getResources().getBoolean(R.bool.config_isDesktopModeSupported); } 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 5aed9e910dd3..9120e0894ccf 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 @@ -93,6 +93,7 @@ import com.android.launcher3.icons.BubbleIconFactory; import com.android.wm.shell.Flags; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.bubbles.bar.BubbleBarDragListener; import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; import com.android.wm.shell.bubbles.shortcut.BubbleShortcutHelper; import com.android.wm.shell.common.DisplayController; @@ -148,7 +149,8 @@ import java.util.function.IntConsumer; * The controller manages addition, removal, and visible state of bubbles on screen. */ public class BubbleController implements ConfigurationChangeListener, - RemoteCallable<BubbleController>, Bubbles.SysuiProxy.Provider { + RemoteCallable<BubbleController>, Bubbles.SysuiProxy.Provider, + BubbleBarDragListener { private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES; @@ -416,7 +418,6 @@ public class BubbleController implements ConfigurationChangeListener, mBubbleData.setListener(mBubbleDataListener); mBubbleData.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged); mDataRepository.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged); - mBubbleData.setPendingIntentCancelledListener(bubble -> { if (bubble.getBubbleIntent() == null) { return; @@ -844,6 +845,47 @@ public class BubbleController implements ConfigurationChangeListener, } } + @Override + public void onDragItemOverBubbleBarDragZone(@Nullable BubbleBarLocation bubbleBarLocation) { + if (bubbleBarLocation == null) return; + if (isShowingAsBubbleBar() && BubbleAnythingFlagHelper.enableCreateAnyBubble()) { + //TODO(b/388894910) show expanded view drop + mBubbleStateListener.onDragItemOverBubbleBarDragZone(bubbleBarLocation); + } + } + + @Override + public void onItemDraggedOutsideBubbleBarDropZone() { + if (isShowingAsBubbleBar() && BubbleAnythingFlagHelper.enableCreateAnyBubble()) { + //TODO(b/388894910) hide expanded view drop + mBubbleStateListener.onItemDraggedOutsideBubbleBarDropZone(); + } + } + + @Override + public void onItemDroppedOverBubbleBarDragZone(@Nullable BubbleBarLocation bubbleBarLocation) { + if (bubbleBarLocation == null) return; + if (isShowingAsBubbleBar() && BubbleAnythingFlagHelper.enableCreateAnyBubble()) { + //TODO(b/388894910) handle item drop with expandStackAndSelectBubble() + } + } + + @Override + public Map<BubbleBarLocation, Rect> getBubbleBarDropZones(int l, int t, int r, int b) { + Map<BubbleBarLocation, Rect> result = new HashMap<>(); + if (isShowingAsBubbleBar() && BubbleAnythingFlagHelper.enableCreateAnyBubble()) { + // TODO(b/393172431) : Utilise DragZoneFactory once it is ready + final int bubbleBarDropZoneSideSize = getContext().getResources().getDimensionPixelSize( + R.dimen.bubble_bar_drop_zone_side_size); + int top = t - bubbleBarDropZoneSideSize; + result.put(BubbleBarLocation.LEFT, + new Rect(l, top, l + bubbleBarDropZoneSideSize, b)); + result.put(BubbleBarLocation.RIGHT, + new Rect(r - bubbleBarDropZoneSideSize, top, r, b)); + } + return result; + } + /** Whether this userId belongs to the current user. */ private boolean isCurrentProfile(int userId) { return userId == UserHandle.USER_ALL diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDragListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDragListener.kt new file mode 100644 index 000000000000..00eaad675350 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDragListener.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles.bar + +import android.graphics.Rect +import com.android.wm.shell.shared.bubbles.BubbleBarLocation + +/** Controller that takes care of the bubble bar drag events. */ +interface BubbleBarDragListener { + + /** Called when the drag event is over the bubble bar drop zone. */ + fun onDragItemOverBubbleBarDragZone(location: BubbleBarLocation) + + /** Called when the drag event leaves the bubble bar drop zone. */ + fun onItemDraggedOutsideBubbleBarDropZone() + + /** Called when the drop event happens over the bubble bar drop zone. */ + fun onItemDroppedOverBubbleBarDragZone(location: BubbleBarLocation?) + + /** + * Returns mapping of the bubble bar locations to the corresponding + * [rect][android.graphics.Rect] zone. + */ + fun getBubbleBarDropZones(l: Int, t: Int, r: Int, b: Int): Map<BubbleBarLocation, Rect> +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 6f16e047a968..6ab103e3bd89 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -53,6 +53,7 @@ import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser; import com.android.wm.shell.apptoweb.AssistContentRequester; import com.android.wm.shell.appzoomout.AppZoomOutController; import com.android.wm.shell.back.BackAnimationController; +import com.android.wm.shell.bubbles.bar.BubbleBarDragListener; import com.android.wm.shell.bubbles.BubbleController; import com.android.wm.shell.bubbles.BubbleData; import com.android.wm.shell.bubbles.BubbleDataRepository; @@ -169,19 +170,18 @@ import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationPromo import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController; import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + import dagger.Binds; import dagger.Lazy; import dagger.Module; import dagger.Provides; - import kotlinx.coroutines.CoroutineScope; import kotlinx.coroutines.ExperimentalCoroutinesApi; import kotlinx.coroutines.MainCoroutineDispatcher; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - /** * Provides dependencies from {@link com.android.wm.shell}, these dependencies are only accessible * from components within the WM subcomponent (can be explicitly exposed to the SysUIComponent, see @@ -1157,9 +1157,10 @@ public abstract class WMShellModule { @WMSingleton @Provides static DesksTransitionObserver provideDesksTransitionObserver( - @NonNull @DynamicOverride DesktopUserRepositories desktopUserRepositories + @NonNull @DynamicOverride DesktopUserRepositories desktopUserRepositories, + @NonNull DesksOrganizer desksOrganizer ) { - return new DesksTransitionObserver(desktopUserRepositories); + return new DesksTransitionObserver(desktopUserRepositories, desksOrganizer); } @WMSingleton @@ -1411,6 +1412,7 @@ public abstract class WMShellModule { IconProvider iconProvider, GlobalDragListener globalDragListener, Transitions transitions, + Lazy<BubbleController> bubbleControllerLazy, @ShellMainThread ShellExecutor mainExecutor) { return new DragAndDropController( context, @@ -1423,6 +1425,12 @@ public abstract class WMShellModule { iconProvider, globalDragListener, transitions, + new Lazy<>() { + @Override + public BubbleBarDragListener get() { + return bubbleControllerLazy.get(); + } + }, mainExecutor); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt index a4620d5a4dfe..b8dbbf9543d2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt @@ -23,6 +23,7 @@ import android.os.IBinder import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_CHANGE import android.view.animation.DecelerateInterpolator +import android.window.DesktopModeFlags import android.window.DesktopModeFlags.ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS import android.window.TransitionInfo import android.window.TransitionRequestInfo @@ -396,7 +397,7 @@ class DesktopImmersiveController( taskId = taskId, immersive = pendingTransition.direction == Direction.ENTER, ) - if (Flags.enableRestoreToPreviousSizeFromDesktopImmersive()) { + if (DesktopModeFlags.ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE.isTrue) { when (pendingTransition.direction) { Direction.EXIT -> { desktopRepository.removeBoundsBeforeFullImmersive(taskId) @@ -457,7 +458,7 @@ class DesktopImmersiveController( val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: error("Expected non-null display layout for displayId: ${taskInfo.displayId}") - return if (Flags.enableRestoreToPreviousSizeFromDesktopImmersive()) { + return if (DesktopModeFlags.ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE.isTrue) { desktopUserRepositories.current.removeBoundsBeforeFullImmersive(taskInfo.taskId) ?: if (ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isTrue()) { calculateInitialBounds(displayLayout, taskInfo) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt index b93d2e396402..03bc42f08d59 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt @@ -57,7 +57,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl return false } if (!Flags.enableMultipleDesktopsBackend()) { - return controller.moveTaskToDesktop(taskId, transitionSource = UNKNOWN) + return controller.moveTaskToDefaultDeskAndActivate(taskId, transitionSource = UNKNOWN) } if (args.size < 3) { pw.println("Error: desk id should be provided as arguments") @@ -70,8 +70,9 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl pw.println("Error: desk id should be an integer") return false } + controller.moveTaskToDesk(taskId = taskId, deskId = deskId, transitionSource = UNKNOWN) pw.println("Not implemented.") - return false + return true } private fun runMoveToNextDisplay(args: Array<String>, pw: PrintWriter): Boolean { @@ -131,8 +132,8 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl pw.println("Error: desk id should be an integer") return false } - pw.println("Not implemented.") - return false + controller.activateDesk(deskId) + return true } private fun runRemoveDesk(args: Array<String>, pw: PrintWriter): Boolean { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt index 043b353ba380..4777e7f93bc9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt @@ -226,31 +226,42 @@ class DesktopRepository( desktopData.setActiveDesk(displayId = displayId, deskId = deskId) } + /** Returns the id of the active desk in the given display, if any. */ + @VisibleForTesting + fun getActiveDeskId(displayId: Int): Int? = desktopData.getActiveDesk(displayId)?.deskId + /** * Adds task with [taskId] to the list of freeform tasks on [displayId]'s active desk. * * TODO: b/389960283 - add explicit [deskId] argument. */ fun addTask(displayId: Int, taskId: Int, isVisible: Boolean) { - addOrMoveFreeformTaskToTop(displayId, taskId) - addActiveTask(displayId, taskId) - updateTask(displayId, taskId, isVisible) + val activeDesk = + checkNotNull(desktopData.getDefaultDesk(displayId)) { + "Expected desk in display: $displayId" + } + addTaskToDesk(displayId = displayId, deskId = activeDesk.deskId, taskId = taskId, isVisible) } - /** - * Adds task with [taskId] to the list of active tasks on [displayId]'s active desk. - * - * TODO: b/389960283 - add explicit [deskId] argument. - */ - private fun addActiveTask(displayId: Int, taskId: Int) { - val activeDesk = desktopData.getDefaultDesk(displayId) - checkNotNull(activeDesk) { "Expected desk in display: $displayId" } + fun addTaskToDesk(displayId: Int, deskId: Int, taskId: Int, isVisible: Boolean) { + addOrMoveTaskToTopOfDesk(displayId = displayId, deskId = deskId, taskId = taskId) + addActiveTaskToDesk(displayId = displayId, deskId = deskId, taskId = taskId) + updateTaskInDesk( + displayId = displayId, + deskId = deskId, + taskId = taskId, + isVisible = isVisible, + ) + } + + private fun addActiveTaskToDesk(displayId: Int, deskId: Int, taskId: Int) { + val desk = checkNotNull(desktopData.getDesk(deskId)) { "Did not find desk: $deskId" } - // Removes task if it is active on another desk excluding [activeDesk]. - removeActiveTask(taskId, excludedDeskId = activeDesk.deskId) + // Removes task if it is active on another desk excluding this desk. + removeActiveTask(taskId, excludedDeskId = deskId) - if (activeDesk.activeTasks.add(taskId)) { - logD("Adds active task=%d displayId=%d deskId=%d", taskId, displayId, activeDesk.deskId) + if (desk.activeTasks.add(taskId)) { + logD("Adds active task=%d displayId=%d deskId=%d", taskId, displayId, deskId) updateActiveTasksListeners(displayId) } } @@ -401,10 +412,10 @@ class DesktopRepository( emptySet() } - /** Removes task from visible tasks of all displays except [excludedDisplayId]. */ - private fun removeVisibleTask(taskId: Int, excludedDisplayId: Int? = null) { + /** Removes task from visible tasks of all desks except [excludedDeskId]. */ + private fun removeVisibleTask(taskId: Int, excludedDeskId: Int? = null) { desktopData.forAllDesks { displayId, desk -> - if (displayId != excludedDisplayId && desk.visibleTasks.remove(taskId)) { + if (desk.deskId != excludedDeskId && desk.visibleTasks.remove(taskId)) { notifyVisibleTaskListeners(displayId, desk.visibleTasks.size) } } @@ -419,30 +430,58 @@ class DesktopRepository( * TODO: b/389960283 - add explicit [deskId] argument. */ fun updateTask(displayId: Int, taskId: Int, isVisible: Boolean) { - logD("updateTask taskId=%d, displayId=%d, isVisible=%b", taskId, displayId, isVisible) + val validDisplayId = + if (displayId == INVALID_DISPLAY) { + // When a task vanishes it doesn't have a displayId. Find the display of the task. + getDisplayIdForTask(taskId) + } else { + displayId + } + if (validDisplayId == null) { + logW("No display id found for task: taskId=%d", taskId) + return + } + val desk = + checkNotNull(desktopData.getDefaultDesk(validDisplayId)) { + "Expected a desk in display: $validDisplayId" + } + updateTaskInDesk( + displayId = validDisplayId, + deskId = desk.deskId, + taskId = taskId, + isVisible, + ) + } + + private fun updateTaskInDesk(displayId: Int, deskId: Int, taskId: Int, isVisible: Boolean) { + check(displayId != INVALID_DISPLAY) { "Display must be valid" } + logD( + "updateTaskInDesk taskId=%d, deskId=%d, displayId=%d, isVisible=%b", + taskId, + deskId, + displayId, + isVisible, + ) if (isVisible) { - // If task is visible, remove it from any other display besides [displayId]. - removeVisibleTask(taskId, excludedDisplayId = displayId) - } else if (displayId == INVALID_DISPLAY) { - // Task has vanished. Check which display to remove the task from. - removeVisibleTask(taskId) - return + // If task is visible, remove it from any other desk besides [deskId]. + removeVisibleTask(taskId, excludedDeskId = deskId) } - val prevCount = getVisibleTaskCount(displayId) + val desk = checkNotNull(desktopData.getDesk(deskId)) { "Did not find desk: $deskId" } + val prevCount = getVisibleTaskCountInDesk(deskId) if (isVisible) { - desktopData.getDefaultDesk(displayId)?.visibleTasks?.add(taskId) - ?: error("Expected non-null desk in display $displayId") + desk.visibleTasks.add(taskId) unminimizeTask(displayId, taskId) } else { - desktopData.getActiveDesk(displayId)?.visibleTasks?.remove(taskId) + desk.visibleTasks.remove(taskId) } - val newCount = getVisibleTaskCount(displayId) + val newCount = getVisibleTaskCount(deskId) if (prevCount != newCount) { logD( - "Update task visibility taskId=%d visible=%b displayId=%d", + "Update task visibility taskId=%d visible=%b deskId=%d displayId=%d", taskId, isVisible, + deskId, displayId, ) logD("VisibleTaskCount has changed from %d to %d", prevCount, newCount) @@ -602,33 +641,32 @@ class DesktopRepository( /** * Gets number of visible freeform tasks on given [displayId]'s active desk. * - * TODO: b/389960283 - add explicit [deskId] argument. + * TODO: b/389960283 - migrate callers to [getVisibleTaskCountInDesk]. */ fun getVisibleTaskCount(displayId: Int): Int = (desktopData.getActiveDesk(displayId)?.visibleTasks?.size ?: 0).also { logD("getVisibleTaskCount=$it") } + /** Gets the number of visible tasks on the given desk. */ + fun getVisibleTaskCountInDesk(deskId: Int): Int = + desktopData.getDesk(deskId)?.visibleTasks?.size ?: 0 + /** * Adds task (or moves if it already exists) to the top of the ordered list. * * Unminimizes the task if it is minimized. - * - * TODO: b/389960283 - add explicit [deskId] argument. */ - private fun addOrMoveFreeformTaskToTop(displayId: Int, taskId: Int) { - val desk = getDefaultDesk(displayId) ?: error("Expected a desk in display: $displayId") - logD( - "Add or move task to top: display=%d taskId=%d deskId=%d", - taskId, - displayId, - desk.deskId, - ) + private fun addOrMoveTaskToTopOfDesk(displayId: Int, deskId: Int, taskId: Int) { + val desk = desktopData.getDesk(deskId) ?: error("Could not find desk: $deskId") + logD("addOrMoveTaskToTopOfDesk: display=%d deskId=%d taskId=%d", displayId, deskId, taskId) desktopData.forAllDesks { _, desk1 -> desk1.freeformTasksInZOrder.remove(taskId) } desk.freeformTasksInZOrder.add(0, taskId) + // TODO: double check minimization logic. // Unminimize the task if it is minimized. unminimizeTask(displayId, taskId) if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { + // TODO: can probably just update the desk. updatePersistentRepository(displayId) } } @@ -644,6 +682,7 @@ class DesktopRepository( // mark it as minimized. getDisplayIdForTask(taskId)?.let { minimizeTask(it, taskId) } ?: logW("Minimize task: No display id found for task: taskId=%d", taskId) + return } else { logD("Minimize Task: display=%d, task=%d", displayId, taskId) desktopData.getActiveDesk(displayId)?.minimizedTasks?.add(taskId) @@ -676,7 +715,7 @@ class DesktopRepository( private fun getDisplayIdForTask(taskId: Int): Int? { var displayForTask: Int? = null desktopData.forAllDesks { displayId, desk -> - if (taskId in desk.freeformTasksInZOrder) { + if (taskId in desk.activeTasks) { displayForTask = displayId } } 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 475515053bfe..7b0fb1d89557 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 @@ -315,24 +315,10 @@ class DesktopTasksController( } /** Show all tasks, that are part of the desktop, on top of launcher */ + @Deprecated("Use activateDesk() instead.", ReplaceWith("activateDesk()")) fun showDesktopApps(displayId: Int, remoteTransition: RemoteTransition? = null) { logV("showDesktopApps") - val wct = WindowContainerTransaction() - bringDesktopAppsToFront(displayId, wct) - - val transitionType = transitionType(remoteTransition) - val handler = - remoteTransition?.let { - OneShotRemoteHandler(transitions.mainExecutor, remoteTransition) - } - transitions.startTransition(transitionType, wct, handler).also { t -> - handler?.setTransition(t) - } - - // launch from recent DesktopTaskView - desktopModeEnterExitTransitionListener?.onEnterDesktopModeTransitionStarted( - FREEFORM_ANIMATION_DURATION - ) + activateDefaultDeskInDisplay(displayId, remoteTransition) } /** Gets number of visible freeform tasks in [displayId]. */ @@ -371,15 +357,15 @@ class DesktopTasksController( 0 -> return // Full screen case 1 -> - moveRunningTaskToDesktop( - allFocusedTasks.single(), + moveTaskToDefaultDeskAndActivate( + allFocusedTasks.single().taskId, transitionSource = transitionSource, ) // Split-screen case where there are two focused tasks, then we find the child // task to move to desktop. 2 -> - moveRunningTaskToDesktop( - getSplitFocusedTask(allFocusedTasks[0], allFocusedTasks[1]), + moveTaskToDefaultDeskAndActivate( + getSplitFocusedTask(allFocusedTasks[0], allFocusedTasks[1]).taskId, transitionSource = transitionSource, ) else -> @@ -442,15 +428,57 @@ class DesktopTasksController( /** Moves task to desktop mode if task is running, else launches it in desktop mode. */ @JvmOverloads - fun moveTaskToDesktop( + fun moveTaskToDefaultDeskAndActivate( + taskId: Int, + wct: WindowContainerTransaction = WindowContainerTransaction(), + transitionSource: DesktopModeTransitionSource, + remoteTransition: RemoteTransition? = null, + callback: IMoveToDesktopCallback? = null, + ): Boolean { + val runningTask = shellTaskOrganizer.getRunningTaskInfo(taskId) + val backgroundTask = recentTasksController?.findTaskInBackground(taskId) + if (runningTask == null && backgroundTask == null) { + logW("moveTaskToDefaultDeskAndActivate taskId=%d not found", taskId) + return false + } + // TODO(342378842): Instead of using default display, support multiple displays + val displayId = runningTask?.displayId ?: DEFAULT_DISPLAY + val deskId = + checkNotNull(taskRepository.getDefaultDeskId(displayId)) { + "Expected a default desk to exist" + } + return moveTaskToDesk( + taskId = taskId, + deskId = deskId, + wct = wct, + transitionSource = transitionSource, + remoteTransition = remoteTransition, + ) + } + + /** Moves task to desktop mode if task is running, else launches it in desktop mode. */ + fun moveTaskToDesk( taskId: Int, + deskId: Int, wct: WindowContainerTransaction = WindowContainerTransaction(), transitionSource: DesktopModeTransitionSource, remoteTransition: RemoteTransition? = null, callback: IMoveToDesktopCallback? = null, ): Boolean { val runningTask = shellTaskOrganizer.getRunningTaskInfo(taskId) - if (runningTask == null) { + if (runningTask != null) { + moveRunningTaskToDesk( + task = runningTask, + deskId = deskId, + wct = wct, + transitionSource = transitionSource, + remoteTransition = remoteTransition, + callback = callback, + ) + } + val backgroundTask = recentTasksController?.findTaskInBackground(taskId) + if (backgroundTask != null) { + // TODO: b/391484662 - add support for |deskId|. return moveBackgroundTaskToDesktop( taskId, wct, @@ -459,8 +487,8 @@ class DesktopTasksController( callback, ) } - moveRunningTaskToDesktop(runningTask, wct, transitionSource, remoteTransition, callback) - return true + logW("moveTaskToDesk taskId=%d not found", taskId) + return false } private fun moveBackgroundTaskToDesktop( @@ -514,8 +542,9 @@ class DesktopTasksController( } /** Moves a running task to desktop. */ - fun moveRunningTaskToDesktop( + private fun moveRunningTaskToDesk( task: RunningTaskInfo, + deskId: Int, wct: WindowContainerTransaction = WindowContainerTransaction(), transitionSource: DesktopModeTransitionSource, remoteTransition: RemoteTransition? = null, @@ -525,20 +554,49 @@ class DesktopTasksController( logW("Cannot enter desktop for taskId %d, ineligible top activity found", task.taskId) return } - logV("moveRunningTaskToDesktop taskId=%d", task.taskId) + val displayId = taskRepository.getDisplayForDesk(deskId) + logV( + "moveRunningTaskToDesk taskId=%d deskId=%d displayId=%d", + task.taskId, + deskId, + displayId, + ) exitSplitIfApplicable(wct, task) val exitResult = desktopImmersiveController.exitImmersiveIfApplicable( wct = wct, - displayId = task.displayId, + displayId = displayId, excludeTaskId = task.taskId, reason = DesktopImmersiveController.ExitReason.TASK_LAUNCH, ) - // Bring other apps to front first val taskIdToMinimize = - bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId) - addMoveToDesktopChanges(wct, task) + if (Flags.enableMultipleDesktopsBackend()) { + // Activate the desk first. + prepareForDeskActivation(displayId, wct) + desksOrganizer.activateDesk(wct, deskId) + if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { + // TODO: 362720497 - do non-running tasks need to be restarted with + // |wct#startTask|? + } + taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( + doesAnyTaskRequireTaskbarRounding(displayId) + ) + // TODO: 362720497 - activating a desk with the intention to move a new task to it + // means we may need to minimize something in the activating desk. Do so here + // similar + // to how it's done in #bringDesktopAppsToFrontBeforeShowingNewTask instead of + // returning null. + null + } else { + // Bring other apps to front first. + bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId) + } + if (Flags.enableMultipleDesktopsBackend()) { + prepareMoveTaskToDesk(wct, task, deskId) + } else { + addMoveToDesktopChanges(wct, task) + } val transition: IBinder if (remoteTransition != null) { @@ -557,6 +615,18 @@ class DesktopTasksController( addPendingMinimizeTransition(transition, it, MinimizeReason.TASK_LIMIT) } exitResult.asExit()?.runOnTransitionStart?.invoke(transition) + if (Flags.enableMultipleDesktopsBackend()) { + desksTransitionObserver.addPendingTransition( + DeskTransition.ActiveDeskWithTask( + token = transition, + displayId = displayId, + deskId = deskId, + enterTaskId = task.taskId, + ) + ) + } else { + taskRepository.setActiveDesk(displayId = displayId, deskId = deskId) + } } private fun invokeCallbackToOverview(transition: IBinder, callback: IMoveToDesktopCallback?) { @@ -606,9 +676,9 @@ class DesktopTasksController( val wct = WindowContainerTransaction() exitSplitIfApplicable(wct, taskInfo) if (Flags.enablePerDisplayDesktopWallpaperActivity()) { - moveHomeTask(wct, toTop = true, taskInfo.displayId) + moveHomeTask(taskInfo.displayId, wct) } else { - moveHomeTask(wct, toTop = true) + moveHomeTask(context.displayId, wct) } val taskIdToMinimize = bringDesktopAppsToFrontBeforeShowingNewTask(taskInfo.displayId, wct, taskInfo.taskId) @@ -780,7 +850,7 @@ class DesktopTasksController( // We are moving a freeform task to fullscreen, put the home task under the fullscreen task. if (!forceEnterDesktop(task.displayId)) { - moveHomeTask(wct, toTop = true, task.displayId) + moveHomeTask(task.displayId, wct) wct.reorder(task.token, /* onTop= */ true) } @@ -1018,6 +1088,23 @@ class DesktopTasksController( } val wct = WindowContainerTransaction() + + // check if the task is part of splitscreen + if ( + Flags.enableNonDefaultDisplaySplit() && + Flags.enableMoveToNextDisplayShortcut() && + splitScreenController.isTaskInSplitScreen(task.taskId) + ) { + val stageCoordinatorRootTaskToken = + splitScreenController.multiDisplayProvider.getDisplayRootForDisplayId( + DEFAULT_DISPLAY + ) + + wct.reparent(stageCoordinatorRootTaskToken, displayAreaInfo.token, true /* onTop */) + transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null) + return + } + if (!task.isFreeform) { addMoveToDesktopChanges(wct, task, displayId) } else if (Flags.enableMoveToNextDisplayShortcut()) { @@ -1037,6 +1124,10 @@ class DesktopTasksController( task.displayId, wct, forceToFullscreen = false, + // TODO: b/371096166 - Temporary turing home relaunch off to prevent home stealing + // display focus. Remove shouldEndUpAtHome = false when home focus handling + // with connected display is implemented in wm core. + shouldEndUpAtHome = false, ) } @@ -1416,33 +1507,36 @@ class DesktopTasksController( ?: WINDOWING_MODE_UNDEFINED } + private fun prepareForDeskActivation(displayId: Int, wct: WindowContainerTransaction) { + // Move home to front, ensures that we go back home when all desktop windows are closed + val useParamDisplayId = + Flags.enableMultipleDesktopsBackend() || + Flags.enablePerDisplayDesktopWallpaperActivity() + moveHomeTask(displayId = if (useParamDisplayId) displayId else context.displayId, wct = wct) + // Currently, we only handle the desktop on the default display really. + if ( + (displayId == DEFAULT_DISPLAY || Flags.enablePerDisplayDesktopWallpaperActivity()) && + ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() + ) { + // Add translucent wallpaper activity to show the wallpaper underneath. + addWallpaperActivity(displayId, wct) + } + } + private fun bringDesktopAppsToFrontBeforeShowingNewTask( displayId: Int, wct: WindowContainerTransaction, newTaskIdInFront: Int, ): Int? = bringDesktopAppsToFront(displayId, wct, newTaskIdInFront) + @Deprecated("Use activeDesk() instead.", ReplaceWith("activateDesk()")) private fun bringDesktopAppsToFront( displayId: Int, wct: WindowContainerTransaction, newTaskIdInFront: Int? = null, ): Int? { logV("bringDesktopAppsToFront, newTaskId=%d", newTaskIdInFront) - // Move home to front, ensures that we go back home when all desktop windows are closed - if (Flags.enablePerDisplayDesktopWallpaperActivity()) { - moveHomeTask(wct, toTop = true, displayId) - } else { - moveHomeTask(wct, toTop = true) - } - - // Currently, we only handle the desktop on the default display really. - if ( - (displayId == DEFAULT_DISPLAY || Flags.enablePerDisplayDesktopWallpaperActivity()) && - ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() - ) { - // Add translucent wallpaper activity to show the wallpaper underneath - addWallpaperActivity(displayId, wct) - } + prepareForDeskActivation(displayId, wct) val expandedTasksOrderedFrontToBack = taskRepository.getExpandedTasksOrdered(displayId) // If we're adding a new Task we might need to minimize an old one @@ -1486,15 +1580,11 @@ class DesktopTasksController( return taskIdToMinimize } - private fun moveHomeTask( - wct: WindowContainerTransaction, - toTop: Boolean, - displayId: Int = DEFAULT_DISPLAY, - ) { + private fun moveHomeTask(displayId: Int, wct: WindowContainerTransaction) { shellTaskOrganizer .getRunningTasks(displayId) .firstOrNull { task -> task.activityType == ACTIVITY_TYPE_HOME } - ?.let { homeTask -> wct.reorder(homeTask.getToken(), /* onTop= */ toTop) } + ?.let { homeTask -> wct.reorder(homeTask.getToken(), /* onTop= */ true) } } private fun addLaunchHomePendingIntent(wct: WindowContainerTransaction, displayId: Int) { @@ -2150,6 +2240,7 @@ class DesktopTasksController( * different [displayId] if the task should be moved to a different display. */ @VisibleForTesting + @Deprecated("Deprecated with multiple desks", ReplaceWith("prepareMoveTaskToDesk()")) fun addMoveToDesktopChanges( wct: WindowContainerTransaction, taskInfo: RunningTaskInfo, @@ -2177,6 +2268,24 @@ class DesktopTasksController( } } + private fun prepareMoveTaskToDesk( + wct: WindowContainerTransaction, + taskInfo: RunningTaskInfo, + deskId: Int, + ) { + if (!Flags.enableMultipleDesktopsBackend()) return + val displayId = taskRepository.getDisplayForDesk(deskId) + val displayLayout = displayController.getDisplayLayout(displayId) ?: return + val initialBounds = getInitialBounds(displayLayout, taskInfo, displayId) + if (canChangeTaskPosition(taskInfo)) { + wct.setBounds(taskInfo.token, initialBounds) + } + desksOrganizer.moveTaskToDesk(wct, deskId = deskId, task = taskInfo) + if (useDesktopOverrideDensity()) { + wct.setDensityDpi(taskInfo.token, DESKTOP_DENSITY_OVERRIDE) + } + } + /** * Apply changes to move a freeform task from one display to another, which includes handling * density changes between displays. @@ -2372,6 +2481,57 @@ class DesktopTasksController( ) } + private fun activateDefaultDeskInDisplay( + displayId: Int, + remoteTransition: RemoteTransition? = null, + ) { + val deskId = + checkNotNull(taskRepository.getDefaultDeskId(displayId)) { + "Expected a default desk to exist" + } + activateDesk(deskId, remoteTransition) + } + + /** Activates the given desk. */ + fun activateDesk(deskId: Int, remoteTransition: RemoteTransition? = null) { + val displayId = taskRepository.getDisplayForDesk(deskId) + val wct = WindowContainerTransaction() + if (Flags.enableMultipleDesktopsBackend()) { + prepareForDeskActivation(displayId, wct) + desksOrganizer.activateDesk(wct, deskId) + if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { + // TODO: 362720497 - do non-running tasks need to be restarted with |wct#startTask|? + } + taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( + doesAnyTaskRequireTaskbarRounding(displayId) + ) + } else { + bringDesktopAppsToFront(displayId, wct) + } + + val transitionType = transitionType(remoteTransition) + val handler = + remoteTransition?.let { + OneShotRemoteHandler(transitions.mainExecutor, remoteTransition) + } + + val transition = transitions.startTransition(transitionType, wct, handler) + handler?.setTransition(transition) + if (Flags.enableMultipleDesktopsBackend()) { + desksTransitionObserver.addPendingTransition( + DeskTransition.ActivateDesk( + token = transition, + displayId = displayId, + deskId = deskId, + ) + ) + } + + desktopModeEnterExitTransitionListener?.onEnterDesktopModeTransitionStarted( + FREEFORM_ANIMATION_DURATION + ) + } + /** Removes the default desk in the given display. */ @Deprecated("Deprecated with multi-desks.", ReplaceWith("removeDesk()")) fun removeDefaultDeskInDisplay(displayId: Int) { @@ -3120,7 +3280,7 @@ class DesktopTasksController( callback: IMoveToDesktopCallback?, ) { executeRemoteCallWithTaskPermission(controller, "moveTaskToDesktop") { c -> - c.moveTaskToDesktop( + c.moveTaskToDefaultDeskAndActivate( taskId, transitionSource = transitionSource, remoteTransition = remoteTransition, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt index d061e03b9be5..3af52b35bed7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt @@ -22,6 +22,7 @@ import androidx.datastore.core.CorruptionException import androidx.datastore.core.DataStore import androidx.datastore.core.DataStoreFactory import androidx.datastore.core.Serializer +import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler import androidx.datastore.dataStoreFile import com.android.framework.protobuf.InvalidProtocolBufferException import com.android.internal.annotations.VisibleForTesting @@ -48,6 +49,10 @@ constructor(private val dataStore: DataStore<WindowingEducationProto>) { DataStoreFactory.create( serializer = WindowingEducationProtoSerializer, produceFile = { context.dataStoreFile(APP_HANDLE_EDUCATION_DATASTORE_FILEPATH) }, + corruptionHandler = + ReplaceFileCorruptionHandler( + produceNewData = { WindowingEducationProto.getDefaultInstance() } + ), ) ) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppToWebEducationDatastoreRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppToWebEducationDatastoreRepository.kt index e5ad901d1435..f16428dfb90b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppToWebEducationDatastoreRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppToWebEducationDatastoreRepository.kt @@ -22,6 +22,7 @@ import androidx.datastore.core.CorruptionException import androidx.datastore.core.DataStore import androidx.datastore.core.DataStoreFactory import androidx.datastore.core.Serializer +import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler import androidx.datastore.dataStoreFile import com.android.framework.protobuf.InvalidProtocolBufferException import com.android.internal.annotations.VisibleForTesting @@ -42,6 +43,10 @@ constructor(private val dataStore: DataStore<WindowingEducationProto>) { DataStoreFactory.create( serializer = WindowingEducationProtoSerializer, produceFile = { context.dataStoreFile(APP_TO_WEB_EDUCATION_DATASTORE_FILEPATH) }, + corruptionHandler = + ReplaceFileCorruptionHandler( + produceNewData = { WindowingEducationProto.getDefaultInstance() } + ), ) ) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt index 47088c0b545a..8c4fd9db050f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt @@ -30,4 +30,16 @@ sealed class DeskTransition { val tasks: Set<Int>, val onDeskRemovedListener: OnDeskRemovedListener?, ) : DeskTransition() + + /** A transition to activate a desk in its display. */ + data class ActivateDesk(override val token: IBinder, val displayId: Int, val deskId: Int) : + DeskTransition() + + /** A transition to activate a desk by moving an outside task to it. */ + data class ActiveDeskWithTask( + override val token: IBinder, + val displayId: Int, + val deskId: Int, + val enterTaskId: Int, + ) : DeskTransition() } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt index 5cbb59fbf323..547890a6200a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt @@ -43,6 +43,9 @@ interface DesksOrganizer { */ fun getDeskAtEnd(change: TransitionInfo.Change): Int? + /** Whether the desk is activate according to the given change at the end of a transition. */ + fun isDeskActiveAtEnd(change: TransitionInfo.Change, deskId: Int): Boolean + /** A callback that is invoked when the desk container is created. */ fun interface OnCreateCallback { /** Calls back when the [deskId] has been created. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt index 3e49b8a4538b..6d88c3310a63 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt @@ -25,7 +25,10 @@ import com.android.wm.shell.desktopmode.DesktopUserRepositories * Observer of desk-related transitions, such as adding, removing or activating a whole desk. It * tracks pending transitions and updates repository state once they finish. */ -class DesksTransitionObserver(private val desktopUserRepositories: DesktopUserRepositories) { +class DesksTransitionObserver( + private val desktopUserRepositories: DesktopUserRepositories, + private val desksOrganizer: DesksOrganizer, +) { private val deskTransitions = mutableMapOf<IBinder, DeskTransition>() /** Adds a pending desk transition to be tracked. */ @@ -53,6 +56,38 @@ class DesksTransitionObserver(private val desktopUserRepositories: DesktopUserRe desktopRepository.removeDesk(deskTransition.deskId) deskTransition.onDeskRemovedListener?.onDeskRemoved(displayId, deskId) } + is DeskTransition.ActivateDesk -> { + val activeDeskChange = + info.changes.find { change -> + desksOrganizer.isDeskActiveAtEnd(change, deskTransition.deskId) + } + activeDeskChange?.let { + desktopRepository.setActiveDesk( + displayId = deskTransition.displayId, + deskId = deskTransition.deskId, + ) + } + } + is DeskTransition.ActiveDeskWithTask -> { + val withTask = + info.changes.find { change -> + change.taskInfo?.taskId == deskTransition.enterTaskId && + change.taskInfo?.isVisibleRequested == true && + desksOrganizer.getDeskAtEnd(change) == deskTransition.deskId + } + withTask?.let { + desktopRepository.setActiveDesk( + displayId = deskTransition.displayId, + deskId = deskTransition.deskId, + ) + desktopRepository.addTaskToDesk( + displayId = deskTransition.displayId, + deskId = deskTransition.deskId, + taskId = deskTransition.enterTaskId, + isVisible = true, + ) + } + } } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt index 79c48c5e9594..5cda76e2f3e0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt @@ -22,6 +22,7 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.util.SparseArray import android.view.SurfaceControl +import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.TransitionInfo import android.window.WindowContainerTransaction import androidx.core.util.forEach @@ -88,12 +89,18 @@ class RootTaskDesksOrganizer( task: RunningTaskInfo, ) { val root = roots[deskId] ?: error("Root not found for desk: $deskId") + wct.setWindowingMode(task.token, WINDOWING_MODE_UNDEFINED) wct.reparent(task.token, root.taskInfo.token, /* onTop= */ true) } override fun getDeskAtEnd(change: TransitionInfo.Change): Int? = change.taskInfo?.parentTaskId?.takeIf { it in roots } + override fun isDeskActiveAtEnd(change: TransitionInfo.Change, deskId: Int): Boolean = + change.taskInfo?.taskId == deskId && + change.taskInfo?.isVisibleRequested == true && + change.mode == TRANSIT_TO_FRONT + override fun onTaskAppeared(taskInfo: RunningTaskInfo, leash: SurfaceControl) { if (taskInfo.parentTaskId in roots) { val deskId = taskInfo.parentTaskId diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt index 9e41270c21f8..1566544f5303 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt @@ -24,6 +24,7 @@ import androidx.datastore.core.CorruptionException import androidx.datastore.core.DataStore import androidx.datastore.core.DataStoreFactory import androidx.datastore.core.Serializer +import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler import androidx.datastore.dataStoreFile import com.android.framework.protobuf.InvalidProtocolBufferException import com.android.wm.shell.shared.annotations.ShellBackgroundThread @@ -49,6 +50,10 @@ class DesktopPersistentRepository(private val dataStore: DataStore<DesktopPersis serializer = DesktopPersistentRepositoriesSerializer, produceFile = { context.dataStoreFile(DESKTOP_REPOSITORIES_DATASTORE_FILE) }, scope = bgCoroutineScope, + corruptionHandler = + ReplaceFileCorruptionHandler( + produceNewData = { DesktopPersistentRepositories.getDefaultInstance() } + ), ) ) @@ -127,7 +132,10 @@ class DesktopPersistentRepository(private val dataStore: DataStore<DesktopPersis .toBuilder() .putDesktopRepoByUser( userId, - currentRepository.toBuilder().putDesktop(desktopId, desktop.build()).build(), + currentRepository + .toBuilder() + .putDesktop(desktopId, desktop.build()) + .build(), ) .build() } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java index e8996bc03eeb..a67557bd7bd0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java @@ -62,6 +62,7 @@ import com.android.internal.protolog.ProtoLog; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.bubbles.bar.BubbleBarDragListener; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; @@ -80,6 +81,8 @@ import java.util.ArrayList; import java.util.function.Consumer; import java.util.function.Function; +import dagger.Lazy; + /** * Handles the global drag and drop handling for the Shell. */ @@ -101,6 +104,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll private final GlobalDragListener mGlobalDragListener; private final Transitions mTransitions; private SplitScreenController mSplitScreen; + private Lazy<BubbleBarDragListener> mBubbleBarDragController; private ShellExecutor mMainExecutor; private ArrayList<DragAndDropListener> mListeners = new ArrayList<>(); @@ -143,6 +147,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll IconProvider iconProvider, GlobalDragListener globalDragListener, Transitions transitions, + Lazy<BubbleBarDragListener> bubbleBarDragController, ShellExecutor mainExecutor) { mContext = context; mShellController = shellController; @@ -153,6 +158,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll mIconProvider = iconProvider; mGlobalDragListener = globalDragListener; mTransitions = transitions; + mBubbleBarDragController = bubbleBarDragController; mMainExecutor = mainExecutor; shellInit.addInitCallback(this::onInit, this); } @@ -246,7 +252,8 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll R.layout.global_drop_target, null); rootView.setOnDragListener(this); rootView.setVisibility(View.INVISIBLE); - DragLayoutProvider dragLayout = new DragLayout(context, mSplitScreen, mIconProvider); + DragLayoutProvider dragLayout = new DragLayout(context, mSplitScreen, + mBubbleBarDragController.get(), mIconProvider); dragLayout.addDraggingView(rootView); try { wm.addView(rootView, layoutParams); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java index 5c72cb7f71a6..f0e0295336a7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java @@ -44,10 +44,8 @@ import android.graphics.Color; import android.graphics.Insets; import android.graphics.Point; import android.graphics.Rect; -import android.graphics.RectF; import android.graphics.Region; import android.graphics.drawable.Drawable; -import android.util.Log; import android.view.DragEvent; import android.view.SurfaceControl; import android.view.View; @@ -66,9 +64,11 @@ import com.android.internal.logging.InstanceId; import com.android.internal.protolog.ProtoLog; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.R; +import com.android.wm.shell.bubbles.bar.BubbleBarDragListener; import com.android.wm.shell.common.split.SplitScreenUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.animation.Interpolators; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import com.android.wm.shell.splitscreen.SplitScreenController; import java.io.PrintWriter; @@ -106,9 +106,11 @@ public class DragLayout extends LinearLayout private boolean mIsLeftRightSplit; private SplitDragPolicy.Target mCurrentTarget = null; + private final BubbleBarDragListener mBubbleBarDragListener; + private final Map<BubbleBarLocation, Rect> mBubbleBarLocations = new HashMap<>(); + private BubbleBarLocation mCurrentBubbleBarTarget = null; private DropZoneView mDropZoneView1; private DropZoneView mDropZoneView2; - private int mDisplayMargin; private int mDividerSize; private int mLaunchIntentEdgeMargin; @@ -128,11 +130,14 @@ public class DragLayout extends LinearLayout // Used with enableFlexibleSplit() flag @SuppressLint("WrongConstant") - public DragLayout(Context context, SplitScreenController splitScreenController, + public DragLayout(Context context, + SplitScreenController splitScreenController, + BubbleBarDragListener bubbleBarDragListener, IconProvider iconProvider) { super(context); mSplitScreenController = splitScreenController; mIconProvider = iconProvider; + mBubbleBarDragListener = bubbleBarDragListener; mPolicy = new SplitDragPolicy(context, splitScreenController, this); mStatusBarManager = context.getSystemService(StatusBarManager.class); mLastConfiguration.setTo(context.getResources().getConfiguration()); @@ -188,6 +193,12 @@ public class DragLayout extends LinearLayout protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); updateTouchableRegion(); + updateBubbleBarRegions(l, t, r, b); + } + + private void updateBubbleBarRegions(int l, int t, int r, int b) { + mBubbleBarLocations.clear(); + mBubbleBarLocations.putAll(mBubbleBarDragListener.getBubbleBarDropZones(l, t, r, b)); } /** @@ -514,17 +525,18 @@ public class DragLayout extends LinearLayout if (mHasDropped) { return; } + // if event is over the bubble don't let split handle it + if (interceptBubbleBarEvent(x, y)) { + mLastPosition.set(x, y); + return; + } // Find containing region, if the same as mCurrentRegion, then skip, otherwise, animate the // visibility of the current region SplitDragPolicy.Target target = mPolicy.getTargetAtLocation(x, y); if (mCurrentTarget != target) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Current target: %s", target); if (target == null) { - // Animating to no target - animateSplitContainers(false, null /* animCompleteCallback */); - if (enableFlexibleSplit()) { - animateHighlight(target); - } + animateToNoTarget(); } else if (mCurrentTarget == null) { if (mPolicy.getNumTargets() == 1) { animateFullscreenContainer(true); @@ -565,6 +577,45 @@ public class DragLayout extends LinearLayout mLastPosition.set(x, y); } + private boolean interceptBubbleBarEvent(int x, int y) { + BubbleBarLocation bubbleBarLocation = getBubbleBarLocation(x, y); + boolean isOverTheBubbleBar = bubbleBarLocation != null; + if (mCurrentBubbleBarTarget != bubbleBarLocation) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Current bubble bar location: %s", + isOverTheBubbleBar); + mCurrentBubbleBarTarget = bubbleBarLocation; + if (isOverTheBubbleBar) { + mBubbleBarDragListener.onDragItemOverBubbleBarDragZone(bubbleBarLocation); + if (mCurrentTarget != null) { + animateToNoTarget(); + mCurrentTarget = null; + } + } else { + mBubbleBarDragListener.onItemDraggedOutsideBubbleBarDropZone(); + } + //TODO(b/388894910): handle accessibility + } + return isOverTheBubbleBar; + } + + @Nullable + private BubbleBarLocation getBubbleBarLocation(int x, int y) { + for (BubbleBarLocation location : mBubbleBarLocations.keySet()) { + if (mBubbleBarLocations.get(location).contains(x, y)) { + return location; + } + } + return null; + } + + private void animateToNoTarget() { + // Animating to no target + animateSplitContainers(false, null /* animCompleteCallback */); + if (enableFlexibleSplit()) { + animateHighlight(null); + } + } + /** * Hides the drag layout and animates out the visible drop targets. */ @@ -596,11 +647,13 @@ public class DragLayout extends LinearLayout */ public boolean drop(DragEvent event, @NonNull SurfaceControl dragSurface, @Nullable WindowContainerToken hideTaskToken, Runnable dropCompleteCallback) { - final boolean handledDrop = mCurrentTarget != null; + final boolean handledDrop = mCurrentTarget != null || mCurrentBubbleBarTarget != null; mHasDropped = true; // Process the drop mPolicy.onDropped(mCurrentTarget, hideTaskToken); + //TODO(b/388894910) add info about the application + mBubbleBarDragListener.onItemDroppedOverBubbleBarDragZone(mCurrentBubbleBarTarget); // Start animating the drop UI out with the drag surface hide(event, dropCompleteCallback); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitMultiDisplayProvider.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitMultiDisplayProvider.java new file mode 100644 index 000000000000..d2e57e51762b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitMultiDisplayProvider.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2025 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.splitscreen; + +import android.window.WindowContainerToken; + +public interface SplitMultiDisplayProvider { + /** + * Returns the WindowContainerToken for the root of the given display ID. + * + * @param displayId The ID of the display. + * @return The {@link WindowContainerToken} associated with the display's root task. + */ + WindowContainerToken getDisplayRootForDisplayId(int displayId); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index ae0159263364..e9f8a4a86d27 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -321,6 +321,10 @@ public class SplitScreenController implements SplitDragPolicy.Starter, return mStageCoordinator; } + public SplitMultiDisplayProvider getMultiDisplayProvider() { + return mStageCoordinator; + } + @Nullable public ActivityManager.RunningTaskInfo getTaskInfo(@SplitPosition int splitPosition) { if (!isSplitScreenVisible() || splitPosition == SPLIT_POSITION_UNDEFINED) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 6783df8f8324..13940b1da257 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -189,7 +189,8 @@ import java.util.function.Predicate; */ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, DisplayController.OnDisplaysChangedListener, Transitions.TransitionHandler, - ShellTaskOrganizer.TaskListener, StageTaskListener.StageListenerCallbacks { + ShellTaskOrganizer.TaskListener, StageTaskListener.StageListenerCallbacks, + SplitMultiDisplayProvider { private static final String TAG = StageCoordinator.class.getSimpleName(); @@ -287,6 +288,16 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitTransitions.registerSplitAnimListener(listener, executor); } + @Override + public WindowContainerToken getDisplayRootForDisplayId(int displayId) { + if (displayId == DEFAULT_DISPLAY) { + return mRootTaskInfo != null ? mRootTaskInfo.token : null; + } + + // TODO(b/393217881): support different root task on external displays. + return null; // Return null for unknown display IDs + } + class SplitRequest { @SplitPosition int mActivatePosition; 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 fb4ce13c441f..dd1edeafde5d 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 @@ -755,7 +755,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, // App sometimes draws before the insets from WindowDecoration#relayout have // been added, so they must be added here decoration.addCaptionInset(wct); - mDesktopTasksController.moveTaskToDesktop(taskId, wct, source, + mDesktopTasksController.moveTaskToDefaultDeskAndActivate(taskId, wct, source, /* remoteTransition= */ null, /* moveToDesktopCallback */ null); decoration.closeHandleMenu(); @@ -904,8 +904,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, * Whether to pilfer the next motion event to send cancellations to the windows below. * Useful when the caption window is spy and the gesture should be handled by the system * instead of by the app for their custom header content. - * Should not have any effect when {@link Flags#enableAccessibleCustomHeaders()}, because - * a spy window is not used then. + * Should not have any effect when + * {@link DesktopModeFlags#ENABLE_ACCESSIBLE_CUSTOM_HEADERS}, because a spy window is not + * used then. */ private boolean mIsCustomHeaderGesture; private boolean mIsResizeGesture; @@ -1047,7 +1048,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, return false; } if (mInputManager != null - && !Flags.enableAccessibleCustomHeaders()) { + && !DesktopModeFlags.ENABLE_ACCESSIBLE_CUSTOM_HEADERS.isTrue()) { ViewRootImpl viewRootImpl = v.getViewRootImpl(); if (viewRootImpl != null) { // Pilfer so that windows below receive cancellations for this gesture. 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 afb234899339..2232e6865b00 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 @@ -935,7 +935,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin // The app is requesting to customize the caption bar, which means input on // customizable/exclusion regions must go to the app instead of to the system. // This may be accomplished with spy windows or custom touchable regions: - if (Flags.enableAccessibleCustomHeaders()) { + if (DesktopModeFlags.ENABLE_ACCESSIBLE_CUSTOM_HEADERS.isTrue()) { // Set the touchable region of the caption to only the areas where input should // be handled by the system (i.e. non custom-excluded areas). The region will // be calculated based on occluding caption elements and exclusion areas diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidManifest.xml b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidManifest.xml index 1bbbefadaa03..8fc974d4381e 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidManifest.xml +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidManifest.xml @@ -47,6 +47,8 @@ <uses-permission android:name="android.permission.STATUS_BAR_SERVICE" /> <!-- Allow the test to connect to perfetto trace processor --> <uses-permission android:name="android.permission.INTERNET"/> + <!-- Use trusted virtual displays to emulate an external display --> + <uses-permission android:name="android.permission.ADD_TRUSTED_DISPLAY"/> <!-- Allow the test to write directly to /sdcard/ and connect to trace processor --> <application android:requestLegacyExternalStorage="true" diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt index f767861addf9..9b402734a4c4 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt @@ -546,5 +546,29 @@ class DesktopModeFlickerScenarios { AppWindowBecomesPinned(DESKTOP_MODE_APP), ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }) ) + + val OPEN_APP_WHEN_EXTERNAL_DISPLAY_CONNECTED = + FlickerConfigEntry( + scenarioId = ScenarioId("OPEN_APP_WHEN_EXTERNAL_DISPLAY_CONNECTED"), + extractor = + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + return listOf(transitions + .filter { it.type == TransitionType.OPEN } + .maxByOrNull { it.id }!!) + } + } + ), + assertions = + listOf( + AppWindowBecomesVisible(DESKTOP_MODE_APP), + AppWindowOnTopAtEnd(DESKTOP_MODE_APP), + AppWindowBecomesVisible(DESKTOP_WALLPAPER), + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) } } diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppWithExternalDisplayConnected.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppWithExternalDisplayConnected.kt new file mode 100644 index 000000000000..66d2ea95c67f --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppWithExternalDisplayConnected.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2025 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.flicker + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.OPEN_APP_WHEN_EXTERNAL_DISPLAY_CONNECTED +import com.android.wm.shell.scenarios.OpenAppWithExternalDisplayConnected +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Open an app on the default display when an external display is connected. + * + * Assert that the app launches in desktop mode. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class OpenAppWithExternalDisplayConnected : OpenAppWithExternalDisplayConnected() { + @ExpectedScenarios(["OPEN_APP_WHEN_EXTERNAL_DISPLAY_CONNECTED"]) + @Test + override fun openAppWithExternalDisplayConnected() = super.openAppWithExternalDisplayConnected() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig() + .use(FlickerServiceConfig.DEFAULT) + .use(OPEN_APP_WHEN_EXTERNAL_DISPLAY_CONNECTED) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt index 6a343c56d364..8510441c0557 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt @@ -47,6 +47,7 @@ import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.junit.After +import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -163,6 +164,69 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun addTask_deskDoesNotExist_throws() { + repo.removeDesk(deskId = 0) + + assertThrows(Exception::class.java) { + repo.addTask(displayId = DEFAULT_DISPLAY, taskId = 5, isVisible = true) + } + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun addTaskToDesk_deskDoesNotExist_throws() { + repo.removeDesk(deskId = 2) + + assertThrows(Exception::class.java) { + repo.addTaskToDesk( + displayId = DEFAULT_DISPLAY, + deskId = 2, + taskId = 4, + isVisible = true, + ) + } + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun addTaskToDesk_addsToZOrderList() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 2) + repo.addDesk(DEFAULT_DISPLAY, deskId = 3) + repo.addTaskToDesk(displayId = DEFAULT_DISPLAY, deskId = 2, taskId = 5, isVisible = true) + repo.addTaskToDesk(displayId = DEFAULT_DISPLAY, deskId = 2, taskId = 6, isVisible = true) + repo.addTaskToDesk(displayId = DEFAULT_DISPLAY, deskId = 2, taskId = 7, isVisible = true) + repo.addTaskToDesk(displayId = DEFAULT_DISPLAY, deskId = 3, taskId = 8, isVisible = true) + + val orderedTasks = repo.getFreeformTasksIdsInDeskInZOrder(deskId = 2) + assertThat(orderedTasks[0]).isEqualTo(7) + assertThat(orderedTasks[1]).isEqualTo(6) + assertThat(orderedTasks[2]).isEqualTo(5) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun addTaskToDesk_visible_addsToVisible() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 2) + + repo.addTaskToDesk(displayId = DEFAULT_DISPLAY, deskId = 2, taskId = 5, isVisible = true) + + assertThat(repo.isVisibleTask(5)).isTrue() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun addTaskToDesk_removesFromAllOtherDesks() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 2) + repo.addDesk(DEFAULT_DISPLAY, deskId = 3) + repo.addTaskToDesk(displayId = DEFAULT_DISPLAY, deskId = 2, taskId = 7, isVisible = true) + + repo.addTaskToDesk(displayId = DEFAULT_DISPLAY, deskId = 3, taskId = 7, isVisible = true) + + assertThat(repo.getActiveTaskIdsInDesk(2)).doesNotContain(7) + } + + @Test fun removeActiveTask_notifiesActiveTaskListener() { val listener = TestListener() repo.addActiveTaskListener(listener) @@ -467,8 +531,8 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { val listener = TestVisibilityListener() val executor = TestShellExecutor() repo.addVisibleTasksListener(listener, executor) - repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) - repo.updateTask(DEFAULT_DISPLAY, taskId = 2, isVisible = true) + repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) + repo.addTask(DEFAULT_DISPLAY, taskId = 2, isVisible = true) executor.flushAll() assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(2) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index c10d2afbdc99..aa7944cc837f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -66,6 +66,7 @@ import android.widget.Toast import android.window.DisplayAreaInfo import android.window.IWindowContainerToken import android.window.RemoteTransition +import android.window.TransitionInfo import android.window.TransitionRequestInfo import android.window.WindowContainerToken import android.window.WindowContainerTransaction @@ -519,7 +520,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperDisabled() { val homeTask = setUpHomeTask() val task1 = setUpFreeformTask() @@ -540,6 +544,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperEnabled() { val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() @@ -558,6 +563,29 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun showDesktopApps_deskInactive_bringsToFront_multipleDesksEnabled() { + whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) + .thenReturn(Binder()) + val deskId = 0 + // Make desk inactive by activating another desk. + taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 1) + taskRepository.setActiveDesk(DEFAULT_DISPLAY, deskId = 1) + + controller.activateDesk(deskId, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + // Wallpaper is moved to front. + wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + // Desk is activated. + verify(desksOrganizer).activateDesk(wct, deskId) + } + + @Test fun isDesktopModeShowing_noTasks_returnsFalse() { assertThat(controller.isDesktopModeShowing(displayId = 0)).isFalse() } @@ -631,58 +659,83 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY, ) - @DisableFlags( - /** TODO: b/362720497 - re-enable when activation is implemented. */ - Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND - ) - fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_perDisplayWallpaperEnabled_shouldShowWallpaper() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_perDisplayWallpaperEnabled_bringsTasksToFront() { taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) - val homeTask = setUpHomeTask(SECOND_DISPLAY) + setUpHomeTask(SECOND_DISPLAY) val task1 = setUpFreeformTask(SECOND_DISPLAY) val task2 = setUpFreeformTask(SECOND_DISPLAY) markTaskHidden(task1) markTaskHidden(task2) + assertThat(taskRepository.getExpandedTasksOrdered(SECOND_DISPLAY)).contains(task1.taskId) + assertThat(taskRepository.getExpandedTasksOrdered(SECOND_DISPLAY)).contains(task2.taskId) controller.showDesktopApps(SECOND_DISPLAY, RemoteTransition(TestRemoteTransition())) val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(4) - // Expect order to be from bottom: home, wallpaperIntent, task1, task2 - wct.assertReorderAt(index = 0, homeTask) - wct.assertPendingIntentAt(index = 1, desktopWallpaperIntent) - wct.assertReorderAt(index = 2, task1) - wct.assertReorderAt(index = 3, task2) + wct.assertReorder(task1) + wct.assertReorder(task2) } @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - @DisableFlags( + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY, - /** TODO: b/362720497 - re-enable when activation is implemented. */ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, ) + fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_perDisplayWallpaperEnabled_multipleDesksEnabled_bringsDeskToFront() { + whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) + .thenReturn(Binder()) + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = 2) + setUpHomeTask(SECOND_DISPLAY) + + controller.showDesktopApps(SECOND_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + verify(desksOrganizer).activateDesk(wct, deskId = 2) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY, + ) + fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_perDisplayWallpaperEnabled_shouldShowWallpaper() { + whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) + .thenReturn(Binder()) + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) + setUpHomeTask(SECOND_DISPLAY) + + controller.showDesktopApps(SECOND_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + wct.assertPendingIntent(desktopWallpaperIntent) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags(Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY) fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_shouldNotShowWallpaper() { + whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) + .thenReturn(Binder()) taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val homeTask = setUpHomeTask(SECOND_DISPLAY) - val task1 = setUpFreeformTask(SECOND_DISPLAY) - val task2 = setUpFreeformTask(SECOND_DISPLAY) - markTaskHidden(task1) - markTaskHidden(task2) controller.showDesktopApps(SECOND_DISPLAY, RemoteTransition(TestRemoteTransition())) val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(3) - // Expect order to be from bottom: home, task1, task2 (no wallpaper intent) - wct.assertReorderAt(index = 0, homeTask) - wct.assertReorderAt(index = 1, task1) - wct.assertReorderAt(index = 2, task2) + wct.assertWithoutPendingIntent(desktopWallpaperIntent) } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperDisabled() { val homeTask = setUpHomeTask() val task1 = setUpFreeformTask() @@ -704,7 +757,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @DisableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - /** TODO: b/362720497 - re-enable when activation is implemented. */ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, ) fun showDesktopApps_onSecondaryDisplay_desktopWallpaperDisabled_shouldNotMoveLauncher() { @@ -728,6 +780,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperEnabled() { val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() @@ -746,7 +799,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperDisabled() { val homeTask = setUpHomeTask() val task1 = setUpFreeformTask() @@ -767,6 +823,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperEnabled() { val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() @@ -785,7 +842,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun showDesktopApps_noActiveTasks_reorderHomeToTop_desktopWallpaperDisabled() { val homeTask = setUpHomeTask() @@ -800,6 +860,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_noActiveTasks_addDesktopWallpaper_desktopWallpaperEnabled() { + whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) + .thenReturn(Binder()) controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) val wct = @@ -808,7 +870,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperDisabled() { taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) @@ -831,6 +896,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperEnabled() { + whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) + .thenReturn(Binder()) taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY) @@ -843,17 +910,63 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(3) // Move home to front wct.assertReorderAt(index = 0, homeTaskDefaultDisplay) // Add desktop wallpaper activity wct.assertPendingIntentAt(index = 1, desktopWallpaperIntent) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplayTasks_desktopWallpaperEnabled_multiDesksDisabled() { + whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) + .thenReturn(Binder()) + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) + val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) + val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY) + setUpHomeTask(SECOND_DISPLAY) + val taskSecondDisplay = setUpFreeformTask(SECOND_DISPLAY) + markTaskHidden(taskDefaultDisplay) + markTaskHidden(taskSecondDisplay) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) // Move freeform task to front wct.assertReorderAt(index = 2, taskDefaultDisplay) } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplayTasks_desktopWallpaperEnabled_multiDesksEnabled() { + whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) + .thenReturn(Binder()) + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) + val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) + val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY) + setUpHomeTask(SECOND_DISPLAY) + val taskSecondDisplay = setUpFreeformTask(SECOND_DISPLAY) + markTaskHidden(taskDefaultDisplay) + markTaskHidden(taskSecondDisplay) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + // Move desktop tasks to front + verify(desksOrganizer).activateDesk(wct, deskId = DEFAULT_DISPLAY) + } + + @Test + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun showDesktopApps_desktopWallpaperDisabled_dontReorderMinimizedTask() { val homeTask = setUpHomeTask() val freeformTask = setUpFreeformTask() @@ -874,6 +987,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + /** TODO: b/362720497 - add multi-desk version when minimization is implemented. */ + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun showDesktopApps_desktopWallpaperEnabled_dontReorderMinimizedTask() { val homeTask = setUpHomeTask() val freeformTask = setUpFreeformTask() @@ -1290,11 +1405,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() { val task = setUpFullscreenTask() val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN - controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) val wct = getLatestEnterDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_FREEFORM) @@ -1303,11 +1419,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun moveRunningTaskToDesktop_tdaFreeform_windowingModeSetToUndefined() { val task = setUpFullscreenTask() val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM - controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) val wct = getLatestEnterDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_UNDEFINED) @@ -1316,11 +1433,78 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun moveTaskToDesktop_nonExistentTask_doesNothing() { - controller.moveTaskToDesktop(999, transitionSource = UNKNOWN) - verifyEnterDesktopWCTNotExecuted() - verify(desktopModeEnterExitTransitionListener, times(0)) - .onEnterDesktopModeTransitionStarted(anyInt()) + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveRunningTaskToDesktop_movesTaskToDefaultDesk() { + val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + verify(desksOrganizer).moveTaskToDesk(wct, deskId = 0, task) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveRunningTaskToDesktop_activatesDesk() { + val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + verify(desksOrganizer).activateDesk(wct, deskId = 0) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveRunningTaskToDesktop_triggersEnterDesktopListener() { + val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) + + verify(desktopModeEnterExitTransitionListener) + .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveTaskToDesk_nonDefaultDesk_movesTaskToDesk() { + val transition = Binder() + whenever(enterDesktopTransitionHandler.moveToDesktop(any(), any())).thenReturn(transition) + taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 3) + val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + task.isVisible = true + + controller.moveTaskToDesk(taskId = task.taskId, deskId = 3, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + verify(desksOrganizer).moveTaskToDesk(wct, deskId = 3, task) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveTaskToDesk_nonDefaultDesk_activatesDesk() { + val transition = Binder() + whenever(enterDesktopTransitionHandler.moveToDesktop(any(), any())).thenReturn(transition) + taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 3) + val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + task.isVisible = true + + controller.moveTaskToDesk(taskId = task.taskId, deskId = 3, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + verify(desksOrganizer).activateDesk(wct, deskId = 3) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveTaskToDesk_nonDefaultDesk_triggersEnterDesktopListener() { + taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 3) + val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + + controller.moveTaskToDesk(taskId = task.taskId, deskId = 3, transitionSource = UNKNOWN) + + verify(desktopModeEnterExitTransitionListener) + .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) } @Test @@ -1330,7 +1514,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task) - controller.moveTaskToDesktop(task.taskId, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) with(getLatestEnterDesktopWct()) { assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM) @@ -1344,7 +1528,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task) - controller.moveTaskToDesktop(task.taskId, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) with(getLatestEnterDesktopWct()) { // Add desktop wallpaper activity @@ -1356,7 +1540,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) - fun moveRunningTaskToDesktop_topActivityTranslucentWithoutDisplay_taskIsMovedToDesktop() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveRunningTaskToDesktop_topActivityTranslucentWithoutDisplay_taskIsMovedToDesktop_multiDesksDisabled() { val task = setUpFullscreenTask().apply { isActivityStackTransparent = true @@ -1364,7 +1549,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() numActivities = 1 } - controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) val wct = getLatestEnterDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_FREEFORM) @@ -1373,6 +1558,26 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun moveRunningTaskToDesktop_topActivityTranslucentWithoutDisplay_taskIsMovedToDesktop_multiDesksEnabled() { + val task = + setUpFullscreenTask().apply { + isActivityStackTransparent = true + isTopActivityNoDisplay = true + numActivities = 1 + } + + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + verify(desksOrganizer).moveTaskToDesk(wct, deskId = 0, task = task) + verify(desktopModeEnterExitTransitionListener) + .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) fun moveRunningTaskToDesktop_topActivityTranslucentWithDisplay_doesNothing() { val task = @@ -1382,7 +1587,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() numActivities = 1 } - controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) verifyEnterDesktopWCTNotExecuted() verify(desktopModeEnterExitTransitionListener, times(0)) .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) @@ -1401,13 +1606,14 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() isTopActivityNoDisplay = false } - controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) verifyEnterDesktopWCTNotExecuted() } @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) - fun moveRunningTaskToDesktop_systemUIActivityWithoutDisplay_doesNothing() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveRunningTaskToDesktop_systemUIActivityWithoutDisplay_doesNothing_multiDesksDisabled() { // Set task as systemUI package val systemUIPackageName = context.resources.getString(com.android.internal.R.string.config_systemUi) @@ -1418,7 +1624,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() isTopActivityNoDisplay = true } - controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) val wct = getLatestEnterDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) @@ -1437,7 +1643,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() mContext.setMockPackageManager(packageManager) whenever(packageManager.getHomeActivities(any())).thenReturn(homeActivities) - controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) verifyEnterDesktopWCTNotExecuted() } @@ -1453,7 +1659,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() mContext.setMockPackageManager(packageManager) whenever(packageManager.getHomeActivities(any())).thenReturn(homeActivities) - controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) val wct = getLatestEnterDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) @@ -1461,6 +1667,28 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun moveRunningTaskToDesktop_systemUIActivityWithoutDisplay_doesNothing_multiDesksEnabled() { + // Set task as systemUI package + val systemUIPackageName = + context.resources.getString(com.android.internal.R.string.config_systemUi) + val baseComponent = ComponentName(systemUIPackageName, /* cls= */ "") + val task = + setUpFullscreenTask().apply { + baseActivity = baseComponent + isTopActivityNoDisplay = true + } + + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + verify(desksOrganizer).moveTaskToDesk(wct, deskId = 0, task = task) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun moveBackgroundTaskToDesktop_remoteTransition_usesOneShotHandler() { val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) @@ -1470,7 +1698,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val task = createTaskInfo(1) whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task) - controller.moveTaskToDesktop( + controller.moveTaskToDefaultDeskAndActivate( taskId = task.taskId, transitionSource = UNKNOWN, remoteTransition = RemoteTransition(spy(TestRemoteTransition())), @@ -1487,8 +1715,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())) .thenReturn(Binder()) - controller.moveRunningTaskToDesktop( - task = setUpFullscreenTask(), + controller.moveTaskToDefaultDeskAndActivate( + taskId = setUpFullscreenTask().taskId, transitionSource = UNKNOWN, remoteTransition = RemoteTransition(spy(TestRemoteTransition())), ) @@ -1499,14 +1727,20 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun moveRunningTaskToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperDisabled() { val homeTask = setUpHomeTask() val freeformTask = setUpFreeformTask() val fullscreenTask = setUpFullscreenTask() markTaskHidden(freeformTask) - controller.moveRunningTaskToDesktop(fullscreenTask, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate( + fullscreenTask.taskId, + transitionSource = UNKNOWN, + ) with(getLatestEnterDesktopWct()) { // Operations should include home task, freeform task @@ -1521,12 +1755,16 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun moveRunningTaskToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperEnabled() { val freeformTask = setUpFreeformTask() val fullscreenTask = setUpFullscreenTask() markTaskHidden(freeformTask) - controller.moveRunningTaskToDesktop(fullscreenTask, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate( + fullscreenTask.taskId, + transitionSource = UNKNOWN, + ) with(getLatestEnterDesktopWct()) { // Operations should include wallpaper intent, freeform task, fullscreen task @@ -1542,6 +1780,43 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun moveRunningTaskToDesktop_desktopWallpaperEnabled_multiDesksEnabled() { + val freeformTask = setUpFreeformTask() + val fullscreenTask = setUpFullscreenTask() + markTaskHidden(freeformTask) + + controller.moveTaskToDefaultDeskAndActivate( + fullscreenTask.taskId, + transitionSource = UNKNOWN, + ) + + val wct = getLatestEnterDesktopWct() + wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + verify(desksOrganizer).moveTaskToDesk(wct, deskId = 0, fullscreenTask) + verify(desksOrganizer).activateDesk(wct, deskId = 0) + verify(desktopModeEnterExitTransitionListener) + .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveRunningTaskToDesktop_activatesDesk_desktopWallpaperEnabled_multiDesksDisabled() { + val fullscreenTask = setUpFullscreenTask() + + controller.moveTaskToDefaultDeskAndActivate( + fullscreenTask.taskId, + transitionSource = UNKNOWN, + ) + + assertThat(taskRepository.getActiveDeskId(DEFAULT_DISPLAY)).isEqualTo(DEFAULT_DISPLAY) + } + + @Test fun moveRunningTaskToDesktop_onlyFreeformTasksFromCurrentDisplayBroughtToFront() { setUpHomeTask(displayId = DEFAULT_DISPLAY) val freeformTaskDefault = setUpFreeformTask(displayId = DEFAULT_DISPLAY) @@ -1553,7 +1828,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val freeformTaskSecond = setUpFreeformTask(displayId = SECOND_DISPLAY) markTaskHidden(freeformTaskSecond) - controller.moveRunningTaskToDesktop(fullscreenTaskDefault, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate( + fullscreenTaskDefault.taskId, + transitionSource = UNKNOWN, + ) with(getLatestEnterDesktopWct()) { // Check that hierarchy operations do not include tasks from second display @@ -1567,9 +1845,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun moveRunningTaskToDesktop_splitTaskExitsSplit() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveRunningTaskToDesktop_splitTaskExitsSplit_multiDesksDisabled() { val task = setUpSplitScreenTask() - controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) val wct = getLatestEnterDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_FREEFORM) @@ -1584,12 +1863,27 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveRunningTaskToDesktop_splitTaskExitsSplit_multiDesksEnabled() { + val task = setUpSplitScreenTask() + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + verify(desksOrganizer).moveTaskToDesk(wct, deskId = 0, task) + verify(desktopModeEnterExitTransitionListener) + .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) + verify(splitScreenController) + .prepareExitSplitScreen( + any(), + anyInt(), + eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE), + ) + } + + @Test fun moveRunningTaskToDesktop_fullscreenTaskDoesNotExitSplit() { val task = setUpFullscreenTask() - controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) val wct = getLatestEnterDesktopWct() - assertThat(wct.changes[task.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) verify(desktopModeEnterExitTransitionListener) .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) verify(splitScreenController, never()) @@ -1601,13 +1895,16 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun moveRunningTaskToDesktop_desktopWallpaperDisabled_bringsTasksOver_dontShowBackTask() { val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } val newTask = setUpFullscreenTask() val homeTask = setUpHomeTask() - controller.moveRunningTaskToDesktop(newTask, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate(newTask.taskId, transitionSource = UNKNOWN) val wct = getLatestEnterDesktopWct() verify(desktopModeEnterExitTransitionListener) @@ -1623,12 +1920,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun moveRunningTaskToDesktop_desktopWallpaperEnabled_bringsTasksOverLimit_dontShowBackTask() { val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } val newTask = setUpFullscreenTask() val homeTask = setUpHomeTask() - controller.moveRunningTaskToDesktop(newTask, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate(newTask.taskId, transitionSource = UNKNOWN) val wct = getLatestEnterDesktopWct() verify(desktopModeEnterExitTransitionListener) @@ -3449,7 +3747,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun moveFocusedTaskToDesktop_fullscreenTaskIsMovedToDesktop() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveFocusedTaskToDesktop_fullscreenTaskIsMovedToDesktop_multiDesksDisabled() { val task1 = setUpFullscreenTask() val task2 = setUpFullscreenTask() val task3 = setUpFullscreenTask() @@ -3466,7 +3765,25 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun moveFocusedTaskToDesktop_splitScreenTaskIsMovedToDesktop() { + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveFocusedTaskToDesktop_fullscreenTaskIsMovedToDesktop_multiDesksEnabled() { + val task1 = setUpFullscreenTask() + val task2 = setUpFullscreenTask() + val task3 = setUpFullscreenTask() + + task1.isFocused = true + task2.isFocused = false + task3.isFocused = false + + controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + verify(desksOrganizer).moveTaskToDesk(wct, deskId = 0, task1) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveFocusedTaskToDesktop_splitScreenTaskIsMovedToDesktop_multiDesksDisabled() { val task1 = setUpSplitScreenTask() val task2 = setUpFullscreenTask() val task3 = setUpFullscreenTask() @@ -3493,6 +3810,33 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveFocusedTaskToDesktop_splitScreenTaskIsMovedToDesktop_multiDesksEnabled() { + val task1 = setUpSplitScreenTask() + val task2 = setUpFullscreenTask() + val task3 = setUpFullscreenTask() + val task4 = setUpSplitScreenTask() + + task1.isFocused = true + task2.isFocused = false + task3.isFocused = false + task4.isFocused = true + + task4.parentTaskId = task1.taskId + + controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + verify(desksOrganizer).moveTaskToDesk(wct, deskId = 0, task4) + verify(splitScreenController) + .prepareExitSplitScreen( + any(), + anyInt(), + eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE), + ) + } + + @Test fun moveFocusedTaskToFullscreen() { val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() @@ -3641,6 +3985,59 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun activateDesk_multipleDesks_addsPendingTransition() { + val deskId = 0 + val transition = Binder() + val deskChange = mock(TransitionInfo.Change::class.java) + whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) + .thenReturn(transition) + whenever(desksOrganizer.isDeskActiveAtEnd(deskChange, deskId)).thenReturn(true) + // Make desk inactive by activating another desk. + taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 1) + taskRepository.setActiveDesk(DEFAULT_DISPLAY, deskId = 1) + + controller.activateDesk(deskId, RemoteTransition(TestRemoteTransition())) + + verify(desksTransitionsObserver) + .addPendingTransition( + argThat { + this is DeskTransition.ActivateDesk && + this.token == transition && + this.deskId == 0 + } + ) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun moveTaskToDesk_multipleDesks_addsPendingTransition() { + val transition = Binder() + whenever(enterDesktopTransitionHandler.moveToDesktop(any(), any())).thenReturn(transition) + taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 3) + val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + task.isVisible = true + + controller.moveTaskToDesk(taskId = task.taskId, deskId = 3, transitionSource = UNKNOWN) + + verify(desksTransitionsObserver) + .addPendingTransition( + argThat { + this is DeskTransition.ActiveDeskWithTask && + this.token == transition && + this.deskId == 3 && + this.enterTaskId == task.taskId + } + ) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) fun dragToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() { val spyController = spy(controller) @@ -5011,7 +5408,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() .thenReturn(ExitResult.Exit(exitingTask = 5, runOnTransitionStart = runOnStartTransit)) whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition) - controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate( + taskId = task.taskId, + wct = wct, + transitionSource = UNKNOWN, + ) verify(mMockDesktopImmersiveController) .exitImmersiveIfApplicable(eq(wct), eq(task.displayId), eq(task.taskId), any()) @@ -5035,7 +5436,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() .thenReturn(ExitResult.Exit(exitingTask = 5, runOnTransitionStart = runOnStartTransit)) whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition) - controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate( + taskId = task.taskId, + wct = wct, + transitionSource = UNKNOWN, + ) verify(mMockDesktopImmersiveController) .exitImmersiveIfApplicable(eq(wct), eq(task.displayId), eq(task.taskId), any()) @@ -5617,6 +6022,29 @@ private fun WindowContainerTransaction.assertIndexInBounds(index: Int) { .isGreaterThan(index) } +private fun WindowContainerTransaction.assertHop( + predicate: (WindowContainerTransaction.HierarchyOp) -> Boolean +) { + assertThat(hierarchyOps.any(predicate)).isTrue() +} + +private fun WindowContainerTransaction.assertWithoutHop( + predicate: (WindowContainerTransaction.HierarchyOp) -> Boolean +) { + assertThat(hierarchyOps.none(predicate)).isTrue() +} + +private fun WindowContainerTransaction.assertReorder( + task: RunningTaskInfo, + toTop: Boolean? = null, +) { + assertHop { hop -> + hop.type == HIERARCHY_OP_TYPE_REORDER && + (toTop == null || hop.toTop == toTop) && + hop.container == task.token.asBinder() + } +} + private fun WindowContainerTransaction.assertReorderAt( index: Int, task: RunningTaskInfo, @@ -5678,6 +6106,20 @@ private fun WindowContainerTransaction.hasRemoveAt(index: Int, token: WindowCont assertThat(op.container).isEqualTo(token.asBinder()) } +private fun WindowContainerTransaction.assertPendingIntent(intent: Intent) { + assertHop { hop -> + hop.type == HIERARCHY_OP_TYPE_PENDING_INTENT && + hop.pendingIntent?.intent?.component == intent.component + } +} + +private fun WindowContainerTransaction.assertWithoutPendingIntent(intent: Intent) { + assertWithoutHop { hop -> + hop.type == HIERARCHY_OP_TYPE_PENDING_INTENT && + hop.pendingIntent?.intent?.component == intent.component + } +} + private fun WindowContainerTransaction.assertPendingIntentAt(index: Int, intent: Intent) { assertIndexInBounds(index) val op = hierarchyOps[index] diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt index bfbaa84e9312..9f09e3f57927 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt @@ -21,20 +21,26 @@ import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.view.Display.DEFAULT_DISPLAY import android.view.WindowManager.TRANSIT_CLOSE +import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.TransitionInfo +import android.window.TransitionInfo.Change import androidx.test.filters.SmallTest import com.android.window.flags.Flags import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.desktopmode.DesktopRepository +import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask import com.android.wm.shell.desktopmode.DesktopUserRepositories import com.android.wm.shell.sysui.ShellInit import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever /** * Tests for [DesksTransitionObserver]. @@ -47,6 +53,9 @@ class DesksTransitionObserverTest : ShellTestCase() { @JvmField @Rule val setFlagsRule = SetFlagsRule() + private val mockDesksOrganizer = mock<DesksOrganizer>() + val testScope = TestScope() + private lateinit var desktopUserRepositories: DesktopUserRepositories private lateinit var observer: DesksTransitionObserver @@ -62,10 +71,10 @@ class DesksTransitionObserverTest : ShellTestCase() { /* shellController= */ mock(), /* persistentRepository= */ mock(), /* repositoryInitializer= */ mock(), - /* mainCoroutineScope= */ mock(), + testScope, /* userManager= */ mock(), ) - observer = DesksTransitionObserver(desktopUserRepositories) + observer = DesksTransitionObserver(desktopUserRepositories, mockDesksOrganizer) } @Test @@ -121,4 +130,51 @@ class DesksTransitionObserverTest : ShellTestCase() { assertThat(removeListener.lastDeskRemoved).isEqualTo(5) } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onTransitionReady_activateDesk_updatesRepository() { + val transition = Binder() + val change = Change(mock(), mock()) + whenever(mockDesksOrganizer.isDeskActiveAtEnd(change, deskId = 5)).thenReturn(true) + val activateTransition = + DeskTransition.ActivateDesk(transition, displayId = DEFAULT_DISPLAY, deskId = 5) + repository.addDesk(DEFAULT_DISPLAY, deskId = 5) + + observer.addPendingTransition(activateTransition) + observer.onTransitionReady( + transition = transition, + info = TransitionInfo(TRANSIT_TO_FRONT, /* flags= */ 0).apply { addChange(change) }, + ) + + assertThat(repository.getActiveDeskId(DEFAULT_DISPLAY)).isEqualTo(5) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onTransitionReady_activateDeskWithTask_updatesRepository() = + testScope.runTest { + val deskId = 5 + val task = createFreeformTask(DEFAULT_DISPLAY).apply { isVisibleRequested = true } + val transition = Binder() + val change = Change(mock(), mock()).apply { taskInfo = task } + whenever(mockDesksOrganizer.getDeskAtEnd(change)).thenReturn(deskId) + val activateTransition = + DeskTransition.ActiveDeskWithTask( + transition, + displayId = DEFAULT_DISPLAY, + deskId = deskId, + enterTaskId = task.taskId, + ) + repository.addDesk(DEFAULT_DISPLAY, deskId = deskId) + + observer.addPendingTransition(activateTransition) + observer.onTransitionReady( + transition = transition, + info = TransitionInfo(TRANSIT_TO_FRONT, /* flags= */ 0).apply { addChange(change) }, + ) + + assertThat(repository.getActiveDeskId(DEFAULT_DISPLAY)).isEqualTo(deskId) + assertThat(repository.getActiveTaskIdsInDesk(deskId)).contains(task.taskId) + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt index a07203d86b75..4d4b15389eca 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt @@ -15,9 +15,11 @@ */ package com.android.wm.shell.desktopmode.multidesks +import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.testing.AndroidTestingRunner import android.view.Display import android.view.SurfaceControl +import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.TransitionInfo import android.window.WindowContainerTransaction import android.window.WindowContainerTransaction.HierarchyOp @@ -216,6 +218,13 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } ) .isTrue() + assertThat( + wct.changes.any { change -> + change.key == desktopTask.token.asBinder() && + change.value.windowingMode == WINDOWING_MODE_UNDEFINED + } + ) + .isTrue() } @Test @@ -244,6 +253,26 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { assertThat(endDesk).isEqualTo(freeformRoot.taskId) } + @Test + fun testIsDeskActiveAtEnd() { + organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + freeformRoot.isVisibleRequested = true + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + + val isActive = + organizer.isDeskActiveAtEnd( + change = + TransitionInfo.Change(freeformRoot.token, SurfaceControl()).apply { + taskInfo = freeformRoot + mode = TRANSIT_TO_FRONT + }, + deskId = freeformRoot.taskId, + ) + + assertThat(isActive).isTrue() + } + private class FakeOnCreateCallback : DesksOrganizer.OnCreateCallback { var deskId: Int? = null val created: Boolean diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java index 1b1a5a909220..06dcd8812350 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java @@ -47,6 +47,7 @@ import com.android.internal.logging.UiEventLogger; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.bubbles.bar.BubbleBarDragListener; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.sysui.ShellCommandHandler; @@ -60,6 +61,8 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import dagger.Lazy; + /** * Tests for the drag and drop controller. */ @@ -91,6 +94,8 @@ public class DragAndDropControllerTest extends ShellTestCase { private Transitions mTransitions; @Mock private GlobalDragListener mGlobalDragListener; + @Mock + private Lazy<BubbleBarDragListener> mBubbleBarDragControllerLazy; private DragAndDropController mController; @@ -99,7 +104,8 @@ public class DragAndDropControllerTest extends ShellTestCase { MockitoAnnotations.initMocks(this); mController = new DragAndDropController(mContext, mShellInit, mShellController, mShellCommandHandler, mShellTaskOrganizer, mDisplayController, mUiEventLogger, - mIconProvider, mGlobalDragListener, mTransitions, mMainExecutor); + mIconProvider, mGlobalDragListener, mTransitions, mBubbleBarDragControllerLazy, + mMainExecutor); mController.onInit(); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index 4a91fb429f7b..f15418adf1e3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -639,7 +639,7 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest toDesktopListenerCaptor.value.accept(DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON) - verify(mockDesktopTasksController).moveTaskToDesktop( + verify(mockDesktopTasksController).moveTaskToDefaultDeskAndActivate( eq(decor.mTaskInfo.taskId), any(), eq(DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON), @@ -877,7 +877,7 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest ) verify(mockDesktopTasksController, times(1)) - .moveTaskToDesktop(any(), any(), any(), anyOrNull(), anyOrNull()) + .moveTaskToDefaultDeskAndActivate(any(), any(), any(), anyOrNull(), anyOrNull()) } @Test diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 71013f7f4e34..5dc49a07a6d6 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -10406,6 +10406,23 @@ public class AudioManager { } } + /** + * Enable strict audio hardening (background) enforcement, regardless of release or temporary + * exemptions for debugging purposes. + * Enforced hardening can be found in the audio dumpsys with the API being restricted and the + * level of restriction which was encountered. + * @hide + */ + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) + public void setEnableHardening(boolean shouldEnable) { + final IAudioService service = getService(); + try { + service.setEnableHardening(shouldEnable); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + //==================================================================== // Mute await connection diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 2a740f85aa72..7b8d6663c957 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -819,4 +819,8 @@ interface IAudioService { @EnforcePermission("QUERY_AUDIO_STATE") @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.QUERY_AUDIO_STATE)") boolean shouldNotificationSoundPlay(in AudioAttributes aa); + + @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)") + void setEnableHardening(in boolean shouldEnable); } diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig index 405d292dfafa..2e8c28d8e65b 100644 --- a/media/java/android/media/flags/media_better_together.aconfig +++ b/media/java/android/media/flags/media_better_together.aconfig @@ -21,6 +21,16 @@ flag { } flag { + name: "disable_transfer_when_apps_do_not_support" + namespace: "media_better_together" + description: "Fixes a bug causing output switcher routes to be incorrectly enabled for media transfer." + bug: "373404114" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "enable_audio_input_device_routing_and_volume_control" namespace: "media_better_together" description: "Allows audio input devices routing and volume control via system settings." diff --git a/nfc-non-updatable/flags/flags.aconfig b/nfc-non-updatable/flags/flags.aconfig index 54ded0cddffa..eb30bbe1bfe7 100644 --- a/nfc-non-updatable/flags/flags.aconfig +++ b/nfc-non-updatable/flags/flags.aconfig @@ -198,10 +198,6 @@ flag { bug: "380892385" } -flag { - name: "nfc_hce_latency_events" - is_exported: true - namespace: "wallet_integration" - description: "Enables tracking latency for HCE" - bug: "379849603" -} +# Unless you are adding a flag for a file under nfc-non-updatable, you should +# not add a flag here for Android 16+ targeting features. Use the flags +# in com.android.nfc.module.flags (packages/modules/Nfc/flags) instead. diff --git a/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreferenceGroup.kt b/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreferenceGroup.kt index 75455635fca1..8dd169b1ca86 100644 --- a/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreferenceGroup.kt +++ b/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreferenceGroup.kt @@ -23,7 +23,6 @@ import android.view.View import androidx.preference.Preference import androidx.preference.PreferenceGroup import androidx.preference.PreferenceViewHolder - import com.android.settingslib.widget.preference.banner.R /** @@ -68,6 +67,11 @@ class BannerMessagePreferenceGroup @JvmOverloads constructor( } childPreferences.add(preference) + expandPreference?.let { + it.count = childPreferences.size - 1 + } + updateExpandCollapsePreference() + updateChildrenVisibility() return super.addPreference(preference) } @@ -76,18 +80,40 @@ class BannerMessagePreferenceGroup @JvmOverloads constructor( return false } childPreferences.remove(preference) + expandPreference?.let { + it.count = childPreferences.size - 1 + } + updateChildrenVisibility() + updateExpandCollapsePreference() return super.removePreference(preference) } + override fun removePreferenceRecursively(key: CharSequence): Boolean { + val preference = findPreference<Preference>(key) ?: return false + + if (preference !is BannerMessagePreference) { + return false + } + + childPreferences.remove(preference) + expandPreference?.let { + it.count = childPreferences.size - 1 + } + updateChildrenVisibility() + updateExpandCollapsePreference() + return super.removePreferenceRecursively(key) + } + override fun onBindViewHolder(holder: PreferenceViewHolder) { super.onBindViewHolder(holder) - if (childPreferences.size >= MAX_CHILDREN - 1) { + if (childPreferences.size >= 2) { if (expandPreference == null) { expandPreference = NumberButtonPreference(context).apply { key = expandKey title = expandTitle count = childPreferences.size - 1 btnContentDescription = expandContentDescription + order = EXPAND_ORDER clickListener = View.OnClickListener { toggleExpansion() } @@ -100,6 +126,7 @@ class BannerMessagePreferenceGroup @JvmOverloads constructor( key = collapseKey title = collapseTitle icon = collapseIcon + order = COLLAPSE_ORDER setOnClickListener { toggleExpansion() } @@ -112,14 +139,20 @@ class BannerMessagePreferenceGroup @JvmOverloads constructor( } private fun updateExpandCollapsePreference() { - expandPreference?.isVisible = !isExpanded - collapsePreference?.isVisible = isExpanded + expandPreference?.isVisible = !isExpanded && childPreferences.size > 1 + collapsePreference?.isVisible = isExpanded && childPreferences.size > 1 } private fun updateChildrenVisibility() { - for (i in 1 until childPreferences.size) { + for (i in 0 until childPreferences.size) { val child = childPreferences[i] - child.isVisible = isExpanded + if (i == 0) { + // Make this explicitly visible when e.g. the first BannerMessagePreference + // in the group is dismissed + child.isVisible = true + } else { + child.isVisible = isExpanded + } } } @@ -145,5 +178,9 @@ class BannerMessagePreferenceGroup @JvmOverloads constructor( companion object { private const val MAX_CHILDREN = 3 + // Arbitrary large order numbers for the two preferences + // needed to make sure any Banners are added above them + private const val EXPAND_ORDER = 99 + private const val COLLAPSE_ORDER = 100 } }
\ No newline at end of file diff --git a/packages/SettingsLib/Graph/graph.proto b/packages/SettingsLib/Graph/graph.proto index a834947144a0..ec287c1b65b7 100644 --- a/packages/SettingsLib/Graph/graph.proto +++ b/packages/SettingsLib/Graph/graph.proto @@ -93,6 +93,8 @@ message PreferenceProto { optional PermissionsProto read_permissions = 17; // The required permissions to write preference value. optional PermissionsProto write_permissions = 18; + // Tag constants associated with the preference. + repeated string tags = 19; // Target of an Intent message ActionTarget { diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt index 4290437b0d02..e511bf1c175d 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt @@ -412,6 +412,7 @@ fun PreferenceMetadata.toProto( } metadata.intent(context)?.let { actionTarget = it.toActionTarget(context) } screenMetadata.getLaunchIntent(context, metadata)?.let { launchIntent = it.toProto() } + for (tag in metadata.tags(context)) addTags(tag) } persistent = metadata.isPersistent(context) if (persistent) { diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt index 1e70a32cb38b..a8939ab0d902 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt @@ -89,11 +89,25 @@ interface PreferenceMetadata { /** * Return the extras Bundle object associated with this preference. * - * It is used to provide more information for metadata. + * It is used to provide more *internal* information for metadata. External app is not expected + * to use this information as it could be changed in future. Consider [tags] for external usage. */ fun extras(context: Context): Bundle? = null /** + * Returns the tags associated with this preference. + * + * Unlike [extras], tags are exposed for external usage. The returned tag list must be constants + * and **append only**. Do not edit/delete existing tag strings as it can cause backward + * compatibility issue. + * + * Use cases: + * - identify a specific preference + * - identify a group of preferences related to network settings + */ + fun tags(context: Context): Array<String> = arrayOf() + + /** * Returns if preference is indexable, default value is `true`. * * Return `false` only when the preference is always unavailable on current device. If it is diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java index 4ee9ff059502..ceb6f7b080df 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java @@ -16,8 +16,6 @@ package com.android.settingslib.media; import static android.media.MediaRoute2Info.TYPE_AUX_LINE; -import static android.media.MediaRoute2Info.TYPE_LINE_ANALOG; -import static android.media.MediaRoute2Info.TYPE_LINE_DIGITAL; import static android.media.MediaRoute2Info.TYPE_BLE_HEADSET; import static android.media.MediaRoute2Info.TYPE_BLUETOOTH_A2DP; import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER; @@ -27,6 +25,8 @@ import static android.media.MediaRoute2Info.TYPE_HDMI; import static android.media.MediaRoute2Info.TYPE_HDMI_ARC; import static android.media.MediaRoute2Info.TYPE_HDMI_EARC; import static android.media.MediaRoute2Info.TYPE_HEARING_AID; +import static android.media.MediaRoute2Info.TYPE_LINE_ANALOG; +import static android.media.MediaRoute2Info.TYPE_LINE_DIGITAL; import static android.media.MediaRoute2Info.TYPE_REMOTE_AUDIO_VIDEO_RECEIVER; import static android.media.MediaRoute2Info.TYPE_REMOTE_CAR; import static android.media.MediaRoute2Info.TYPE_REMOTE_COMPUTER; @@ -254,6 +254,10 @@ public abstract class InfoMediaManager { protected abstract List<MediaRoute2Info> getSelectableRoutes(@NonNull RoutingSessionInfo info); @NonNull + protected abstract List<MediaRoute2Info> getTransferableRoutes( + @NonNull RoutingSessionInfo info); + + @NonNull protected abstract List<MediaRoute2Info> getDeselectableRoutes( @NonNull RoutingSessionInfo info); @@ -519,6 +523,22 @@ public abstract class InfoMediaManager { } /** + * Returns the list of {@link MediaDevice media devices} that can be transferred to with the + * current {@link RoutingSessionInfo routing session} by the media route provider. + */ + @NonNull + List<MediaDevice> getTransferableMediaDevices() { + final RoutingSessionInfo info = getActiveRoutingSession(); + + final List<MediaDevice> deviceList = new ArrayList<>(); + for (MediaRoute2Info route : getTransferableRoutes(info)) { + deviceList.add( + new InfoMediaDevice(mContext, route, mPreferenceItemMap.get(route.getId()))); + } + return deviceList; + } + + /** * Returns the list of {@link MediaDevice media devices} that can be deselected from the current * {@link RoutingSessionInfo routing session}. */ diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java index fe6659d1dc4f..76f366d3d1b6 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java @@ -352,6 +352,17 @@ public class LocalMediaManager implements BluetoothCallback { } /** + * Gets the MediaDevice list that can be transferred to with the current media session by the + * media route provider. + * + * @return list of MediaDevice + */ + @NonNull + public List<MediaDevice> getTransferableMediaDevices() { + return mInfoMediaManager.getTransferableMediaDevices(); + } + + /** * Get the MediaDevice list that can be removed from current media session. * * @return list of MediaDevice diff --git a/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java index 82b197682459..9e511ffb4e34 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java @@ -117,6 +117,12 @@ public class ManagerInfoMediaManager extends InfoMediaManager { @Override @NonNull + protected List<MediaRoute2Info> getTransferableRoutes(@NonNull RoutingSessionInfo info) { + return mRouterManager.getTransferableRoutes(info); + } + + @Override + @NonNull protected List<MediaRoute2Info> getDeselectableRoutes(@NonNull RoutingSessionInfo info) { return mRouterManager.getDeselectableRoutes(info); } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java index b01b7c9048ba..d018d1404623 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java @@ -15,6 +15,7 @@ */ package com.android.settingslib.media; +import static android.media.MediaRoute2Info.TYPE_AUX_LINE; import static android.media.MediaRoute2Info.TYPE_BLE_HEADSET; import static android.media.MediaRoute2Info.TYPE_BLUETOOTH_A2DP; import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER; @@ -24,6 +25,8 @@ import static android.media.MediaRoute2Info.TYPE_HDMI; import static android.media.MediaRoute2Info.TYPE_HDMI_ARC; import static android.media.MediaRoute2Info.TYPE_HDMI_EARC; import static android.media.MediaRoute2Info.TYPE_HEARING_AID; +import static android.media.MediaRoute2Info.TYPE_LINE_ANALOG; +import static android.media.MediaRoute2Info.TYPE_LINE_DIGITAL; import static android.media.MediaRoute2Info.TYPE_REMOTE_AUDIO_VIDEO_RECEIVER; import static android.media.MediaRoute2Info.TYPE_REMOTE_SPEAKER; import static android.media.MediaRoute2Info.TYPE_REMOTE_TV; @@ -33,9 +36,6 @@ import static android.media.MediaRoute2Info.TYPE_USB_DEVICE; import static android.media.MediaRoute2Info.TYPE_USB_HEADSET; import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES; import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET; -import static android.media.MediaRoute2Info.TYPE_LINE_DIGITAL; -import static android.media.MediaRoute2Info.TYPE_LINE_ANALOG; -import static android.media.MediaRoute2Info.TYPE_AUX_LINE; import static android.media.RouteListingPreference.Item.FLAG_ONGOING_SESSION; import static android.media.RouteListingPreference.Item.FLAG_ONGOING_SESSION_MANAGED; import static android.media.RouteListingPreference.Item.FLAG_SUGGESTED; @@ -244,6 +244,11 @@ public abstract class MediaDevice implements Comparable<MediaDevice> { */ public abstract String getId(); + /** Returns {@code true} if the device has a non-null {@link RouteListingPreference.Item}. */ + public boolean hasRouteListingPreferenceItem() { + return mItem != null; + } + /** * Get selection behavior of device * diff --git a/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java index 2c7ec9302117..9fe5b1d58752 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java @@ -114,6 +114,12 @@ import java.util.List; @NonNull @Override + protected List<MediaRoute2Info> getTransferableRoutes(@NonNull RoutingSessionInfo info) { + return Collections.emptyList(); + } + + @NonNull + @Override protected List<MediaRoute2Info> getDeselectableRoutes(@NonNull RoutingSessionInfo info) { return Collections.emptyList(); } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java index eced7b3a116b..6a2da182dbb1 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java @@ -203,6 +203,13 @@ public final class RouterInfoMediaManager extends InfoMediaManager { @NonNull @Override + protected List<MediaRoute2Info> getTransferableRoutes(@NonNull RoutingSessionInfo info) { + RoutingController controller = getControllerForSession(info); + return getTransferableRoutes(controller); + } + + @NonNull + @Override protected List<MediaRoute2Info> getSelectedRoutes(@NonNull RoutingSessionInfo info) { RoutingController controller = getControllerForSession(info); if (controller == null) { @@ -272,22 +279,27 @@ public final class RouterInfoMediaManager extends InfoMediaManager { protected List<MediaRoute2Info> getTransferableRoutes(@NonNull String packageName) { List<RoutingController> controllers = mRouter.getControllers(); RoutingController activeController = controllers.get(controllers.size() - 1); - HashMap<String, MediaRoute2Info> transferableRoutes = new HashMap<>(); - - activeController - .getTransferableRoutes() - .forEach(route -> transferableRoutes.put(route.getId(), route)); + return getTransferableRoutes(activeController); + } - if (activeController.getRoutingSessionInfo().isSystemSession()) { - mRouter.getRoutes().stream() - .filter(route -> !route.isSystemRoute()) - .forEach(route -> transferableRoutes.put(route.getId(), route)); - } else { - mRouter.getRoutes().stream() - .filter(route -> route.isSystemRoute()) + @NonNull + private List<MediaRoute2Info> getTransferableRoutes(@Nullable RoutingController controller) { + HashMap<String, MediaRoute2Info> transferableRoutes = new HashMap<>(); + if (controller != null) { + controller + .getTransferableRoutes() .forEach(route -> transferableRoutes.put(route.getId(), route)); - } + if (controller.getRoutingSessionInfo().isSystemSession()) { + mRouter.getRoutes().stream() + .filter(route -> !route.isSystemRoute()) + .forEach(route -> transferableRoutes.put(route.getId(), route)); + } else { + mRouter.getRoutes().stream() + .filter(route -> route.isSystemRoute()) + .forEach(route -> transferableRoutes.put(route.getId(), route)); + } + } return new ArrayList<>(transferableRoutes.values()); } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java index 1a83f0a2e775..219ad6ca3f1a 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java @@ -65,7 +65,8 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; @@ -78,6 +79,7 @@ import java.util.Set; @RunWith(RobolectricTestRunner.class) @Config(shadows = {ShadowRouter2Manager.class}) public class InfoMediaManagerTest { + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); private static final String TEST_PACKAGE_NAME = "com.test.packagename"; private static final String TEST_PACKAGE_NAME_2 = "com.test.packagename2"; @@ -146,7 +148,6 @@ public class InfoMediaManagerTest { @Before public void setUp() { - MockitoAnnotations.initMocks(this); mContext = spy(RuntimeEnvironment.application); doReturn(mMediaSessionManager).when(mContext).getSystemService( @@ -663,6 +664,26 @@ public class InfoMediaManagerTest { } @Test + public void getTransferableMediaDevice_checkList() { + final List<MediaRoute2Info> mediaRoute2Infos = new ArrayList<>(); + final MediaRoute2Info mediaRoute2Info = mock(MediaRoute2Info.class); + mediaRoute2Infos.add(mediaRoute2Info); + mShadowRouter2Manager.setTransferableRoutes(mediaRoute2Infos); + when(mediaRoute2Info.getName()).thenReturn(TEST_NAME); + when(mediaRoute2Info.getId()).thenReturn(TEST_ID); + mInfoMediaManager.mRouterManager = mRouterManager; + when(mRouterManager.getRoutingSessions(TEST_PACKAGE_NAME)) + .thenReturn(List.of(TEST_REMOTE_ROUTING_SESSION)); + when(mRouterManager.getTransferableRoutes(any(RoutingSessionInfo.class))) + .thenReturn(mediaRoute2Infos); + + final List<MediaDevice> mediaDevices = mInfoMediaManager.getTransferableMediaDevices(); + + assertThat(mediaDevices.size()).isEqualTo(1); + assertThat(mediaDevices.get(0).getName()).isEqualTo(TEST_NAME); + } + + @Test public void getDeselectableMediaDevice_checkList() { final List<RoutingSessionInfo> routingSessionInfos = new ArrayList<>(); final RoutingSessionInfo info = mock(RoutingSessionInfo.class); diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 8fe3a0c4b4ae..55f7317f25e4 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -1015,6 +1015,9 @@ <uses-permission android:name="android.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE" /> <uses-permission android:name="android.permission.READ_COLOR_ZONES" /> + <!-- Permission required for trade-in mode testing --> + <uses-permission android:name="android.permission.ENTER_TRADE_IN_MODE" /> + <application android:label="@string/app_label" android:theme="@android:style/Theme.DeviceDefault.DayNight" diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 1f2890c2052e..744388f47d0e 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -425,7 +425,6 @@ android_library { manifest: "AndroidManifest-res.xml", flags_packages: [ "android.app.flags-aconfig", - "com_android_systemui_flags", ], } diff --git a/packages/SystemUI/aconfig/accessibility.aconfig b/packages/SystemUI/aconfig/accessibility.aconfig index fb21be4c3bd1..3cb30258fcb1 100644 --- a/packages/SystemUI/aconfig/accessibility.aconfig +++ b/packages/SystemUI/aconfig/accessibility.aconfig @@ -120,6 +120,16 @@ flag { } flag { + name: "update_window_magnifier_bottom_boundary" + namespace: "accessibility" + description: "Update the window magnifier boundary at the bottom to the top of the system gesture inset." + bug: "380320995" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "hearing_devices_dialog_related_tools" namespace: "accessibility" description: "Shows the related tools for hearing devices dialog." diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt index c8d3430bf54b..f03bd3d9a2a7 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt @@ -73,6 +73,9 @@ import com.android.wm.shell.shared.ShellTransitions import com.android.wm.shell.shared.TransitionUtil import java.util.concurrent.Executor import kotlin.math.roundToInt +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeoutOrNull private const val TAG = "ActivityTransitionAnimator" @@ -241,7 +244,7 @@ constructor( override fun onTransitionAnimationProgress(linearProgress: Float) { LinkedHashSet(listeners).forEach { - it.onTransitionAnimationProgress(linearProgress) + it.onTransitionAnimationProgress(linearProgress) } } @@ -494,15 +497,19 @@ constructor( /** * Create a new animation [Runner] controlled by the [Controller] that [controllerFactory] can - * create based on [forLaunch]. + * create based on [forLaunch] and within the given [scope]. * * This method must only be used for long-lived registrations. Otherwise, use * [createEphemeralRunner]. */ @VisibleForTesting - fun createLongLivedRunner(controllerFactory: ControllerFactory, forLaunch: Boolean): Runner { + fun createLongLivedRunner( + controllerFactory: ControllerFactory, + scope: CoroutineScope, + forLaunch: Boolean, + ): Runner { assertLongLivedReturnAnimations() - return Runner(callback!!, transitionAnimator, lifecycleListener) { + return Runner(scope, callback!!, transitionAnimator, lifecycleListener) { controllerFactory.createController(forLaunch) } } @@ -564,7 +571,7 @@ constructor( * Creates a [Controller] for launching or returning from the activity linked to [cookie] * and [component]. */ - abstract fun createController(forLaunch: Boolean): Controller + abstract suspend fun createController(forLaunch: Boolean): Controller } /** @@ -691,9 +698,14 @@ constructor( * animations. * * The [Controller]s created by [controllerFactory] will only be used for transitions matching - * the [cookie], or the [ComponentName] defined within it if the cookie matching fails. + * the [cookie], or the [ComponentName] defined within it if the cookie matching fails. These + * [Controller]s can only be created within [scope]. */ - fun register(cookie: TransitionCookie, controllerFactory: ControllerFactory) { + fun register( + cookie: TransitionCookie, + controllerFactory: ControllerFactory, + scope: CoroutineScope, + ) { assertLongLivedReturnAnimations() if (transitionRegister == null) { @@ -725,7 +737,7 @@ constructor( } val launchRemoteTransition = RemoteTransition( - OriginTransition(createLongLivedRunner(controllerFactory, forLaunch = true)), + OriginTransition(createLongLivedRunner(controllerFactory, scope, forLaunch = true)), "${cookie}_launchTransition", ) transitionRegister.register(launchFilter, launchRemoteTransition, includeTakeover = true) @@ -749,7 +761,9 @@ constructor( } val returnRemoteTransition = RemoteTransition( - OriginTransition(createLongLivedRunner(controllerFactory, forLaunch = false)), + OriginTransition( + createLongLivedRunner(controllerFactory, scope, forLaunch = false) + ), "${cookie}_returnTransition", ) transitionRegister.register(returnFilter, returnRemoteTransition, includeTakeover = true) @@ -952,7 +966,9 @@ constructor( * Reusable factory to generate single-use controllers. In case of an ephemeral [Runner], * this must be null and [controller] must be defined instead. */ - private val controllerFactory: (() -> Controller)?, + private val controllerFactory: (suspend () -> Controller)?, + /** The scope to use when this runner is based on [controllerFactory]. */ + private val scope: CoroutineScope? = null, private val callback: Callback, /** The animator to use to animate the window transition. */ private val transitionAnimator: TransitionAnimator, @@ -973,13 +989,15 @@ constructor( ) constructor( + scope: CoroutineScope, callback: Callback, transitionAnimator: TransitionAnimator, listener: Listener? = null, - controllerFactory: () -> Controller, + controllerFactory: suspend () -> Controller, ) : this( controller = null, controllerFactory = controllerFactory, + scope = scope, callback = callback, transitionAnimator = transitionAnimator, listener = listener, @@ -994,12 +1012,12 @@ constructor( assert((controller != null).xor(controllerFactory != null)) delegate = null - if (controller != null) { + controller?.let { // Ephemeral launches bundle the runner with the launch request (instead of being // registered ahead of time for later use). This means that there could be a timeout // between creation and invocation, so the delegate needs to exist from the // beginning in order to handle such timeout. - createDelegate() + createDelegate(it) } } @@ -1040,49 +1058,79 @@ constructor( finishedCallback: IRemoteAnimationFinishedCallback?, performAnimation: (AnimationDelegate) -> Unit, ) { - maybeSetUp() - val delegate = delegate - mainExecutor.execute { - if (delegate == null) { - Log.i(TAG, "onAnimationStart called after completion") - // Animation started too late and timed out already. We need to still - // signal back that we're done with it. - finishedCallback?.onAnimationFinished() - } else { - performAnimation(delegate) + val controller = controller + val controllerFactory = controllerFactory + + if (controller != null) { + maybeSetUp(controller) + val success = startAnimation(performAnimation) + if (!success) finishedCallback?.onAnimationFinished() + } else if (controllerFactory != null) { + scope?.launch { + val success = + withTimeoutOrNull(TRANSITION_TIMEOUT) { + setUp(controllerFactory) + startAnimation(performAnimation) + } ?: false + if (!success) finishedCallback?.onAnimationFinished() } + } else { + // This should never happen, as either the controller or factory should always be + // defined. This final call is for safety in case something goes wrong. + Log.wtf(TAG, "initAndRun with neither a controller nor factory") + finishedCallback?.onAnimationFinished() + } + } + + /** Tries to start the animation on the main thread and returns whether it succeeded. */ + @BinderThread + private fun startAnimation(performAnimation: (AnimationDelegate) -> Unit): Boolean { + val delegate = delegate + return if (delegate != null) { + mainExecutor.execute { performAnimation(delegate) } + true + } else { + // Animation started too late and timed out already. + Log.i(TAG, "startAnimation called after completion") + false } } @BinderThread override fun onAnimationCancelled() { val delegate = delegate - mainExecutor.execute { - delegate ?: Log.wtf(TAG, "onAnimationCancelled called after completion") - delegate?.onAnimationCancelled() + if (delegate != null) { + mainExecutor.execute { delegate.onAnimationCancelled() } + } else { + Log.wtf(TAG, "onAnimationCancelled called after completion") } } + /** + * Posts the default animation timeouts. Since this only applies to ephemeral launches, this + * method is a no-op if [controller] is not defined. + */ @VisibleForTesting @UiThread fun postTimeouts() { - maybeSetUp() + controller?.let { maybeSetUp(it) } delegate?.postTimeouts() } @AnyThread - private fun maybeSetUp() { - if (controllerFactory == null || delegate != null) return - createDelegate() + private fun maybeSetUp(controller: Controller) { + if (delegate != null) return + createDelegate(controller) } @AnyThread - private fun createDelegate() { - var controller = controller - val factory = controllerFactory - if (controller == null && factory == null) return + private suspend fun setUp(createController: suspend () -> Controller) { + val controller = createController() + createDelegate(controller) + } - controller = controller ?: factory!!.invoke() + @AnyThread + private fun createDelegate(controller: Controller) { delegate = AnimationDelegate( mainExecutor, diff --git a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt index 195b060932eb..20f1cdf4242c 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt @@ -296,13 +296,6 @@ private class NestedDraggableNode( awaitEachGesture { val down = awaitFirstDown(requireUnconsumed = false) - check(down.position == lastFirstDown) { - "Position from detectDrags() is not the same as position in trackDownPosition()" - } - check(pointersDown.size == 1 && pointersDown.keys.first() == down.id) { - "pointersDown should only contain $down but it contains $pointersDown" - } - var overSlop = 0f val onTouchSlopReached = { change: PointerInputChange, over: Float -> if (draggable.shouldStartDrag(change)) { @@ -342,7 +335,12 @@ private class NestedDraggableNode( check(pointersDown.size > 0) { "pointersDown is empty" } val controller = - draggable.onDragStarted(down.position, sign, pointersDown.size, drag.type) + draggable.onDragStarted( + down.position, + sign, + pointersDown.size.coerceAtLeast(1), + drag.type, + ) if (overSlop != 0f) { onDrag(controller, drag, overSlop, velocityTracker) } diff --git a/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/ContentOverscrollEffect.kt b/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/ContentOverscrollEffect.kt index 49e510791929..cb713ece12a5 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/ContentOverscrollEffect.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/ContentOverscrollEffect.kt @@ -18,6 +18,8 @@ package com.android.compose.gesture.effect import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.SpringSpec +import androidx.compose.animation.core.spring import androidx.compose.foundation.OverscrollEffect import androidx.compose.foundation.gestures.Orientation import androidx.compose.ui.geometry.Offset @@ -46,7 +48,7 @@ open class BaseContentOverscrollEffect( private val animationSpec: AnimationSpec<Float>, ) : ContentOverscrollEffect { /** The [Animatable] that holds the current overscroll value. */ - private val animatable = Animatable(initialValue = 0f, visibilityThreshold = 0.5f) + private val animatable = Animatable(initialValue = 0f) private var lastConverter: SpaceVectorConverter? = null override val overscrollDistance: Float @@ -131,11 +133,29 @@ open class BaseContentOverscrollEffect( launch { val consumed = performFling(velocity) val remaining = velocity - consumed - animatable.animateTo(0f, animationSpec, remaining.toFloat()) + animatable.animateTo( + 0f, + animationSpec.withVisibilityThreshold(1f), + remaining.toFloat(), + ) } } } + private fun <T> AnimationSpec<T>.withVisibilityThreshold( + visibilityThreshold: T + ): AnimationSpec<T> { + return when (this) { + is SpringSpec -> + spring( + stiffness = stiffness, + dampingRatio = dampingRatio, + visibilityThreshold = visibilityThreshold, + ) + else -> this + } + } + protected fun requireConverter(): SpaceVectorConverter { return checkNotNull(lastConverter) { "lastConverter is null, make sure to call requireConverter() only when " + 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 3ffbabb09710..4a4607b6e8fc 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 @@ -159,8 +159,7 @@ fun CommunalContainer( content: CommunalContent, ) { val coroutineScope = rememberCoroutineScope() - val currentSceneKey: SceneKey by - viewModel.currentScene.collectAsStateWithLifecycle(CommunalScenes.Blank) + val currentSceneKey: SceneKey by viewModel.currentScene.collectAsStateWithLifecycle() val touchesAllowed by viewModel.touchesAllowed.collectAsStateWithLifecycle() val backgroundType by viewModel.communalBackground.collectAsStateWithLifecycle( diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/ribbon/ui/composable/Ribbon.kt b/packages/SystemUI/compose/features/src/com/android/systemui/ribbon/ui/composable/Ribbon.kt index daa15929b9ce..377bebc404e7 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/ribbon/ui/composable/Ribbon.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/ribbon/ui/composable/Ribbon.kt @@ -20,8 +20,12 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.ColorMatrix import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.layer.drawLayer import androidx.compose.ui.layout.layout import com.android.compose.modifiers.thenIf import kotlin.math.PI @@ -39,6 +43,9 @@ import kotlin.math.tan * The background color of the strip can be modified by passing a value to the [backgroundColor] or * `null` to remove the strip background. * + * The [colorSaturation] is a function that returns that amount of color saturation to apply to the + * entire ribbon. If it's `1`, the full color will be used, if it's `0` it will be greyscale. + * * Note: this function assumes that it's been placed at the bottom right of its parent by its * caller. It's the caller's responsibility to meet that assumption by actually placing this * composable element at the bottom right. @@ -49,6 +56,7 @@ fun BottomRightCornerRibbon( modifier: Modifier = Modifier, degrees: Int = 45, alpha: Float = 0.6f, + colorSaturation: () -> Float = { 1f }, backgroundColor: Color? = Color.Red, ) { check(degrees in 1..89) @@ -73,6 +81,22 @@ fun BottomRightCornerRibbon( translationY = (h - w * sine + h * cosine) / 2f rotationZ = 360f - degrees } + .drawWithCache { + val layer = + obtainGraphicsLayer().apply { + record { + colorFilter = + ColorFilter.colorMatrix( + colorMatrix = + ColorMatrix().apply { + setToSaturation(colorSaturation()) + } + ) + drawContent() + } + } + onDrawWithContent { drawLayer(layer) } + } .thenIf(backgroundColor != null) { Modifier.background(backgroundColor!!) } .layout { measurable, constraints -> val placeable = measurable.measure(constraints) @@ -87,6 +111,6 @@ fun BottomRightCornerRibbon( ) { placeable.place(leftPadding, 0) } - } + }, ) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt index 6c0c5c7e49b9..40e3000ee8a7 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt @@ -232,6 +232,7 @@ fun SceneContainer( BottomRightCornerRibbon( content = { Text(text = "flexi\uD83E\uDD43", color = Color.White) }, + colorSaturation = { viewModel.ribbonColorSaturation }, modifier = Modifier.align(Alignment.BottomEnd), ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelTest.kt index 2d093bf1630b..f9e88341316e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelTest.kt @@ -26,10 +26,8 @@ import com.android.systemui.brightness.domain.interactor.screenBrightnessInterac import com.android.systemui.brightness.shared.model.GammaBrightness import com.android.systemui.brightness.shared.model.LinearBrightness import com.android.systemui.classifier.domain.interactor.falsingInteractor -import com.android.systemui.common.shared.model.ContentDescription -import com.android.systemui.common.shared.model.Icon -import com.android.systemui.common.shared.model.Text import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.graphics.imageLoader import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn @@ -65,6 +63,7 @@ class BrightnessSliderViewModelTest : SysuiTestCase() { falsingInteractor, supportsMirroring = true, brightnessWarningToast, + imageLoader, ) } } @@ -162,20 +161,21 @@ class BrightnessSliderViewModelTest : SysuiTestCase() { } @Test - fun label() { - assertThat(underTest.label) - .isEqualTo(Text.Resource(R.string.quick_settings_brightness_dialog_title)) - } - - @Test fun icon() { - assertThat(underTest.icon) - .isEqualTo( - Icon.Resource( - R.drawable.ic_brightness_full, - ContentDescription.Resource(underTest.label.res), - ) - ) + assertThat(BrightnessSliderViewModel.getIconForPercentage(0f)) + .isEqualTo(R.drawable.ic_brightness_low) + assertThat(BrightnessSliderViewModel.getIconForPercentage(20f)) + .isEqualTo(R.drawable.ic_brightness_low) + assertThat(BrightnessSliderViewModel.getIconForPercentage(20.1f)) + .isEqualTo(R.drawable.ic_brightness_medium) + assertThat(BrightnessSliderViewModel.getIconForPercentage(50f)) + .isEqualTo(R.drawable.ic_brightness_medium) + assertThat(BrightnessSliderViewModel.getIconForPercentage(79.9f)) + .isEqualTo(R.drawable.ic_brightness_medium) + assertThat(BrightnessSliderViewModel.getIconForPercentage(80f)) + .isEqualTo(R.drawable.ic_brightness_full) + assertThat(BrightnessSliderViewModel.getIconForPercentage(100f)) + .isEqualTo(R.drawable.ic_brightness_full) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSceneRepositoryImplTest.kt index fd0bf4dae198..293d32471713 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSceneRepositoryImplTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2025 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. @@ -21,34 +21,44 @@ import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState import com.android.systemui.SysuiTestCase import com.android.systemui.communal.shared.model.CommunalScenes -import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope -import com.android.systemui.kosmos.testScope -import com.android.systemui.scene.shared.model.sceneDataSource +import com.android.systemui.kosmos.backgroundScope +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.scene.shared.model.SceneDataSource +import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify @SmallTest @RunWith(AndroidJUnit4::class) -class CommunalRepositoryImplTest : SysuiTestCase() { +class CommunalSceneRepositoryImplTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() - private val kosmos = testKosmos() - private val testScope = kosmos.testScope - private val underTest by lazy { - CommunalSceneRepositoryImpl( - kosmos.applicationCoroutineScope, - kosmos.applicationCoroutineScope, - kosmos.sceneDataSource, - ) - } + private val delegator = mock<SceneDataSourceDelegator> {} + + private val Kosmos.underTest by + Kosmos.Fixture { + CommunalSceneRepositoryImpl( + applicationScope = applicationCoroutineScope, + backgroundScope = backgroundScope, + sceneDataSource = delegator, + delegator = delegator, + ) + } @Test fun transitionState_idleByDefault() = - testScope.runTest { + kosmos.runTest { val transitionState by collectLastValue(underTest.transitionState) assertThat(transitionState) .isEqualTo(ObservableTransitionState.Idle(CommunalScenes.Default)) @@ -56,7 +66,7 @@ class CommunalRepositoryImplTest : SysuiTestCase() { @Test fun transitionState_setTransitionState_returnsNewValue() = - testScope.runTest { + kosmos.runTest { val expectedSceneKey = CommunalScenes.Communal underTest.setTransitionState(flowOf(ObservableTransitionState.Idle(expectedSceneKey))) @@ -66,7 +76,7 @@ class CommunalRepositoryImplTest : SysuiTestCase() { @Test fun transitionState_setNullTransitionState_returnsDefaultValue() = - testScope.runTest { + kosmos.runTest { // Set a value for the transition state flow. underTest.setTransitionState( flowOf(ObservableTransitionState.Idle(CommunalScenes.Communal)) @@ -80,4 +90,18 @@ class CommunalRepositoryImplTest : SysuiTestCase() { assertThat(transitionState) .isEqualTo(ObservableTransitionState.Idle(CommunalScenes.Default)) } + + @Test + fun showHubFromPowerButton() = + kosmos.runTest { + fakeKeyguardRepository.setKeyguardShowing(false) + + underTest.showHubFromPowerButton() + + argumentCaptor<SceneDataSource>().apply { + verify(delegator).setDelegate(capture()) + + assertThat(firstValue.currentScene.value).isEqualTo(CommunalScenes.Communal) + } + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt index 4e8a2a349283..49d324b27bb1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt @@ -79,6 +79,7 @@ import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doReturn import org.mockito.kotlin.stub @@ -473,6 +474,51 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { } @Test + @EnableFlags(com.android.settings.flags.Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + fun registerEnabledOnKeyguardCallback_multipleUsers_shouldSendAllUpdates() = + testScope.runTest { + + // Simulate call to register callback when in multiple users setup + biometricManager.stub { + on { registerEnabledOnKeyguardCallback(any()) } doAnswer + { invocation -> + val callback = + invocation.arguments[0] as IBiometricEnabledOnKeyguardCallback + callback.onChanged(true, PRIMARY_USER_ID, TYPE_FACE) + callback.onChanged(true, PRIMARY_USER_ID, TYPE_FINGERPRINT) + callback.onChanged(true, ANOTHER_USER_ID, TYPE_FACE) + callback.onChanged(true, ANOTHER_USER_ID, TYPE_FINGERPRINT) + } + } + authController.stub { + on { isFingerprintEnrolled(anyInt()) } doReturn true + on { isFaceAuthEnrolled(anyInt()) } doReturn true + } + + // Check primary user status + createBiometricSettingsRepository() + var fingerprintAllowed = collectLastValue(underTest.isFingerprintEnrolledAndEnabled) + var faceAllowed = collectLastValue(underTest.isFaceAuthEnrolledAndEnabled) + runCurrent() + + enrollmentChange(UNDER_DISPLAY_FINGERPRINT, PRIMARY_USER_ID, true) + enrollmentChange(FACE, PRIMARY_USER_ID, true) + assertThat(fingerprintAllowed()).isTrue() + assertThat(faceAllowed()).isTrue() + + // Check secondary user status + userRepository.setSelectedUserInfo(ANOTHER_USER) + fingerprintAllowed = collectLastValue(underTest.isFingerprintEnrolledAndEnabled) + faceAllowed = collectLastValue(underTest.isFaceAuthEnrolledAndEnabled) + runCurrent() + + enrollmentChange(UNDER_DISPLAY_FINGERPRINT, ANOTHER_USER_ID, true) + enrollmentChange(FACE, ANOTHER_USER_ID, true) + assertThat(fingerprintAllowed()).isTrue() + assertThat(faceAllowed()).isTrue() + } + + @Test fun devicePolicyControlsFaceAuthenticationEnabledState() = testScope.runTest { faceAuthIsEnrolled() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java index 57ac90648f33..3078a943be32 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java @@ -26,6 +26,7 @@ import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECT import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -643,6 +644,132 @@ public class MediaOutputAdapterTest extends SysuiTestCase { verify(mMediaSwitchingController).addDeviceToPlayMedia(mMediaDevice2); } + @DisableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT) + @Test + public void clickFullItemOfSelectableDevice_flagOff_hasListingPreference_verifyConnectDevice() { + List<MediaDevice> mediaDevices = new ArrayList<>(); + mediaDevices.add(mMediaDevice2); + when(mMediaDevice2.hasRouteListingPreferenceItem()).thenReturn(true); + when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(mediaDevices); + when(mMediaSwitchingController.getTransferableMediaDevices()).thenReturn(List.of()); + when(mMediaSwitchingController.isCurrentOutputDeviceHasSessionOngoing()).thenReturn(false); + mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); + + assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2); + assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue(); + + mViewHolder.mContainerLayout.performClick(); + + verify(mMediaSwitchingController).connectDevice(mMediaDevice2); + } + + @EnableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT) + @Test + public void clickFullItemOfSelectableDevice_flagOn_hasListingPreference_verifyConnectDevice() { + List<MediaDevice> mediaDevices = new ArrayList<>(); + mediaDevices.add(mMediaDevice2); + when(mMediaDevice2.hasRouteListingPreferenceItem()).thenReturn(true); + when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(mediaDevices); + when(mMediaSwitchingController.getTransferableMediaDevices()).thenReturn(List.of()); + when(mMediaSwitchingController.isCurrentOutputDeviceHasSessionOngoing()).thenReturn(false); + mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); + + assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2); + assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue(); + + mViewHolder.mContainerLayout.performClick(); + + verify(mMediaSwitchingController).connectDevice(mMediaDevice2); + } + + @DisableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT) + @Test + public void clickFullItemOfSelectableDevice_flagOff_isTransferable_verifyConnectDevice() { + List<MediaDevice> mediaDevices = new ArrayList<>(); + mediaDevices.add(mMediaDevice2); + when(mMediaDevice2.hasRouteListingPreferenceItem()).thenReturn(false); + when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(mediaDevices); + when(mMediaSwitchingController.getTransferableMediaDevices()).thenReturn(mediaDevices); + when(mMediaSwitchingController.isCurrentOutputDeviceHasSessionOngoing()).thenReturn(false); + mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); + + assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2); + assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue(); + + mViewHolder.mContainerLayout.performClick(); + + verify(mMediaSwitchingController).connectDevice(mMediaDevice2); + } + + @EnableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT) + @Test + public void clickFullItemOfSelectableDevice_flagOn_isTransferable_verifyConnectDevice() { + List<MediaDevice> mediaDevices = new ArrayList<>(); + mediaDevices.add(mMediaDevice2); + when(mMediaDevice2.hasRouteListingPreferenceItem()).thenReturn(false); + when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(mediaDevices); + when(mMediaSwitchingController.getTransferableMediaDevices()).thenReturn(mediaDevices); + when(mMediaSwitchingController.isCurrentOutputDeviceHasSessionOngoing()).thenReturn(false); + mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); + + assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2); + assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue(); + + mViewHolder.mContainerLayout.performClick(); + + verify(mMediaSwitchingController).connectDevice(mMediaDevice2); + } + + @DisableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT) + @Test + public void clickFullItemOfSelectableDevice_flagOff_notTransferable_verifyConnectDevice() { + List<MediaDevice> mediaDevices = new ArrayList<>(); + mediaDevices.add(mMediaDevice2); + when(mMediaDevice2.hasRouteListingPreferenceItem()).thenReturn(false); + when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(mediaDevices); + when(mMediaSwitchingController.getTransferableMediaDevices()).thenReturn(List.of()); + when(mMediaSwitchingController.isCurrentOutputDeviceHasSessionOngoing()).thenReturn(false); + mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); + + assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2); + assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue(); + + mViewHolder.mContainerLayout.performClick(); + + verify(mMediaSwitchingController).connectDevice(mMediaDevice2); + } + + @EnableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT) + @Test + public void clickFullItemOfSelectableDevice_flagOn_notTransferable_verifyNotConnectDevice() { + List<MediaDevice> mediaDevices = new ArrayList<>(); + mediaDevices.add(mMediaDevice2); + when(mMediaDevice2.hasRouteListingPreferenceItem()).thenReturn(false); + when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(mediaDevices); + when(mMediaSwitchingController.getTransferableMediaDevices()).thenReturn(List.of()); + when(mMediaSwitchingController.isCurrentOutputDeviceHasSessionOngoing()).thenReturn(false); + mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); + + assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2); + assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue(); + + mViewHolder.mContainerLayout.performClick(); + + verify(mMediaSwitchingController, never()).connectDevice(any(MediaDevice.class)); + } + @Test public void onGroupActionTriggered_clickSelectedRemoteDevice_triggerUngrouping() { when(mMediaSwitchingController.getSelectableMediaDevice()) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ModesTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ModesTileTest.kt index 7bb28dbabd5e..4aa01eac72a8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ModesTileTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ModesTileTest.kt @@ -28,6 +28,7 @@ import com.android.internal.logging.MetricsLogger import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingManagerFake import com.android.systemui.common.shared.model.asIcon +import com.android.systemui.kosmos.mainCoroutineContext import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.ActivityStarter @@ -126,6 +127,7 @@ class ModesTileTest : SysuiTestCase() { userActionInteractor = ModesTileUserActionInteractor( + kosmos.mainCoroutineContext, inputHandler, dialogDelegate, kosmos.zenModeInteractor, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt index 89b8e9171076..b57dc377b465 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt @@ -30,6 +30,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.asIcon import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.mainCoroutineContext import com.android.systemui.kosmos.testScope import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject import com.android.systemui.qs.tiles.base.actions.qsTileIntentUserInputHandler @@ -62,6 +63,7 @@ class ModesTileUserActionInteractorTest : SysuiTestCase() { private val underTest = ModesTileUserActionInteractor( + kosmos.mainCoroutineContext, inputHandler, mockDialogDelegate, zenModeInteractor, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceSessionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceSessionTest.kt index f2e658dc3759..7bcaeabfee69 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceSessionTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceSessionTest.kt @@ -38,6 +38,7 @@ import com.android.systemui.util.settings.fakeGlobalSettings import com.android.traceur.TraceConfig import com.google.common.truth.Truth import org.junit.Before +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt @@ -126,6 +127,7 @@ class IssueRecordingServiceSessionTest : SysuiTestCase() { verify(iActivityManager).requestBugReportWithExtraAttachments(any()) } + @Ignore("b/392753499") @Test fun sharesTracesDirectly_afterReceivingShareCommand_withTakeBugreportFalse() { underTest.takeBugReport = false diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt index b297bed61ecb..fc13cdaae8ce 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt @@ -16,8 +16,6 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.view -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.junit.runner.RunWith import android.content.ComponentName import android.content.DialogInterface import android.content.Intent @@ -25,6 +23,11 @@ import android.content.applicationContext import android.content.packageManager import android.content.pm.ApplicationInfo import android.content.pm.PackageManager +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.view.View +import android.view.Window +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.Kosmos @@ -43,10 +46,12 @@ import kotlin.test.Test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -250,6 +255,36 @@ class EndCastScreenToOtherDeviceDialogDelegateTest : SysuiTestCase() { assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isTrue() } + @Test + @EnableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END) + fun accessibilityDataSensitive_flagEnabled_appliesSetting() { + createAndSetDelegate(ENTIRE_SCREEN) + + val window = mock<Window>() + val decorView = mock<View>() + whenever(sysuiDialog.window).thenReturn(window) + whenever(window.decorView).thenReturn(decorView) + + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) + + verify(decorView).setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES) + } + + @Test + @DisableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END) + fun accessibilityDataSensitive_flagDisabled_doesNotApplySetting() { + createAndSetDelegate(ENTIRE_SCREEN) + + val window = mock<Window>() + val decorView = mock<View>() + whenever(sysuiDialog.window).thenReturn(window) + whenever(window.decorView).thenReturn(decorView) + + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) + + verify(decorView, never()).setAccessibilityDataSensitive(any()) + } + private fun createAndSetDelegate(state: MediaProjectionState.Projecting) { underTest = EndCastScreenToOtherDeviceDialogDelegate( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt index 9e8f22e331ed..ddac98dde45d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt @@ -18,6 +18,10 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.view import android.content.DialogInterface import android.content.applicationContext +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.view.View +import android.view.Window import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -37,10 +41,13 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.runner.RunWith +import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @@ -132,7 +139,7 @@ class EndGenericCastToOtherDeviceDialogDelegateTest : SysuiTestCase() { verify(sysuiDialog) .setPositiveButton( eq(R.string.cast_to_other_device_stop_dialog_button), - clickListener.capture() + clickListener.capture(), ) // Verify that clicking the button stops the recording @@ -144,6 +151,36 @@ class EndGenericCastToOtherDeviceDialogDelegateTest : SysuiTestCase() { assertThat(kosmos.fakeMediaRouterRepository.lastStoppedDevice).isEqualTo(device) } + @Test + @EnableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END) + fun accessibilityDataSensitive_flagEnabled_appliesSetting() { + createAndSetDelegate() + + val window = mock<Window>() + val decorView = mock<View>() + whenever(sysuiDialog.window).thenReturn(window) + whenever(window.decorView).thenReturn(decorView) + + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) + + verify(decorView).setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES) + } + + @Test + @DisableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END) + fun accessibilityDataSensitive_flagDisabled_doesNotApplySetting() { + createAndSetDelegate() + + val window = mock<Window>() + val decorView = mock<View>() + whenever(sysuiDialog.window).thenReturn(window) + whenever(window.decorView).thenReturn(decorView) + + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) + + verify(decorView, never()).setAccessibilityDataSensitive(any()) + } + private fun createAndSetDelegate(deviceName: String? = null) { underTest = EndGenericCastToOtherDeviceDialogDelegate( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt index 709e0b57c02a..1f91babbfa47 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt @@ -23,6 +23,10 @@ import android.content.Intent import android.content.applicationContext import android.content.packageManager import android.content.pm.ApplicationInfo +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.view.View +import android.view.Window import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -45,6 +49,7 @@ import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -130,7 +135,7 @@ class EndScreenRecordingDialogDelegateTest : SysuiTestCase() { verify(sysuiDialog) .setPositiveButton( eq(R.string.screenrecord_stop_dialog_button), - clickListener.capture() + clickListener.capture(), ) // Verify that clicking the button stops the recording @@ -142,6 +147,36 @@ class EndScreenRecordingDialogDelegateTest : SysuiTestCase() { assertThat(kosmos.screenRecordRepository.stopRecordingInvoked).isTrue() } + @Test + @EnableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END) + fun accessibilityDataSensitive_flagEnabled_appliesSetting() { + createAndSetDelegate(recordedTask = null) + + val window = mock<Window>() + val decorView = mock<View>() + whenever(sysuiDialog.window).thenReturn(window) + whenever(window.decorView).thenReturn(decorView) + + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) + + verify(decorView).setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES) + } + + @Test + @DisableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END) + fun accessibilityDataSensitive_flagDisabled_doesNotApplySetting() { + createAndSetDelegate(recordedTask = null) + + val window = mock<Window>() + val decorView = mock<View>() + whenever(sysuiDialog.window).thenReturn(window) + whenever(window.decorView).thenReturn(decorView) + + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) + + verify(decorView, never()).setAccessibilityDataSensitive(any()) + } + private fun createAndSetDelegate(recordedTask: ActivityManager.RunningTaskInfo?) { underTest = EndScreenRecordingDialogDelegate( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegateTest.kt index 411d306f163c..259d3872cfa2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegateTest.kt @@ -18,6 +18,11 @@ package com.android.systemui.statusbar.chips.sharetoapp.ui.view import android.content.DialogInterface import android.content.applicationContext +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.view.View +import android.view.Window +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.testScope @@ -31,26 +36,26 @@ import kotlin.test.Test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever @SmallTest @OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) class EndGenericShareToAppDialogDelegateTest : SysuiTestCase() { private val kosmos = testKosmos() private val sysuiDialog = mock<SystemUIDialog>() - private val underTest = - EndGenericShareToAppDialogDelegate( - kosmos.endMediaProjectionDialogHelper, - kosmos.applicationContext, - stopAction = kosmos.mediaProjectionChipInteractor::stopProjecting, - ) + private lateinit var underTest: EndGenericShareToAppDialogDelegate @Test fun positiveButton_clickStopsRecording() = kosmos.testScope.runTest { + createAndSetDelegate() underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isFalse() @@ -62,4 +67,43 @@ class EndGenericShareToAppDialogDelegateTest : SysuiTestCase() { assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isTrue() } + + @Test + @EnableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END) + fun accessibilityDataSensitive_flagEnabled_appliesSetting() { + createAndSetDelegate() + + val window = mock<Window>() + val decorView = mock<View>() + whenever(sysuiDialog.window).thenReturn(window) + whenever(window.decorView).thenReturn(decorView) + + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) + + verify(decorView).setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES) + } + + @Test + @DisableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END) + fun accessibilityDataSensitive_flagDisabled_doesNotApplySetting() { + createAndSetDelegate() + + val window = mock<Window>() + val decorView = mock<View>() + whenever(sysuiDialog.window).thenReturn(window) + whenever(window.decorView).thenReturn(decorView) + + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) + + verify(decorView, never()).setAccessibilityDataSensitive(any()) + } + + private fun createAndSetDelegate() { + underTest = + EndGenericShareToAppDialogDelegate( + kosmos.endMediaProjectionDialogHelper, + kosmos.applicationContext, + stopAction = kosmos.mediaProjectionChipInteractor::stopProjecting, + ) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegateTest.kt index 6885a6bd7229..0ae0d178185e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegateTest.kt @@ -23,6 +23,11 @@ import android.content.applicationContext import android.content.packageManager import android.content.pm.ApplicationInfo import android.content.pm.PackageManager +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.view.View +import android.view.Window +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.Kosmos @@ -41,15 +46,18 @@ import kotlin.test.Test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @SmallTest @OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) class EndShareScreenToAppDialogDelegateTest : SysuiTestCase() { private val kosmos = Kosmos().also { it.testCase = this } private val sysuiDialog = mock<SystemUIDialog>() @@ -193,6 +201,40 @@ class EndShareScreenToAppDialogDelegateTest : SysuiTestCase() { assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isTrue() } + @Test + @EnableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END) + fun accessibilityDataSensitive_flagEnabled_appliesSetting() { + createAndSetDelegate(ENTIRE_SCREEN) + whenever(kosmos.packageManager.getApplicationInfo(eq(HOST_PACKAGE), any<Int>())) + .thenThrow(PackageManager.NameNotFoundException()) + + val window = mock<Window>() + val decorView = mock<View>() + whenever(sysuiDialog.window).thenReturn(window) + whenever(window.decorView).thenReturn(decorView) + + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) + + verify(decorView).setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES) + } + + @Test + @DisableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END) + fun accessibilityDataSensitive_flagDisabled_doesNotApplySetting() { + createAndSetDelegate(ENTIRE_SCREEN) + whenever(kosmos.packageManager.getApplicationInfo(eq(HOST_PACKAGE), any<Int>())) + .thenThrow(PackageManager.NameNotFoundException()) + + val window = mock<Window>() + val decorView = mock<View>() + whenever(sysuiDialog.window).thenReturn(window) + whenever(window.decorView).thenReturn(decorView) + + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) + + verify(decorView, never()).setAccessibilityDataSensitive(any()) + } + private fun createAndSetDelegate(state: MediaProjectionState.Projecting) { underTest = EndShareScreenToAppDialogDelegate( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt index 5a66888e4da4..3961e177d1aa 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt @@ -297,6 +297,33 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { } @Test + @EnableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END) + fun stopDialog_flagEnabled_eventEmitted_dialogCannotBeDismissedByTouchOutside() = + kosmos.runTest { + val latestDialogModel by collectLastValue(underTest.stopDialogToShow) + + mediaProjectionRepo.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) + + fakeMediaProjectionRepository.emitProjectionStartedDuringCallAndActivePostCallEvent() + + // Verify that the dialog is shown + assertThat(latestDialogModel) + .isInstanceOf(MediaProjectionStopDialogModel.Shown::class.java) + + val dialogModel = latestDialogModel as MediaProjectionStopDialogModel.Shown + + whenever(dialogModel.dialogDelegate.createDialog()).thenReturn(mockDialog) + + dialogModel.createAndShowDialog() + + verify(mockDialog).show() + + // Verify that setCanceledOnTouchOutside(false) is called + verify(mockDialog).setCanceledOnTouchOutside(false) + } + + @Test fun chip_notProjectingState_isHidden() = testScope.runTest { val latest by collectLastValue(underTest.chip) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt index 9dfc922eb7d0..339f8fac3820 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt @@ -46,6 +46,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.collection.provider.visualStabilityProvider import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.NotificationTestHelper import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun @@ -678,25 +679,32 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test - @DisableFlags(StatusBarNotifChips.FLAG_NAME) - fun testIsSticky_promotedAndExpanded_notifChipsFlagOff_true() { - val notif = Notification.Builder(mContext, "").setSmallIcon(R.drawable.ic_person).build() - notif.flags = FLAG_PROMOTED_ONGOING - val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, notif) - val row = testHelper.createRow().apply { setPinnedStatus(PinnedStatus.PinnedBySystem) } - notifEntry.row = row - - underTest.showNotification(notifEntry) + @DisableFlags(StatusBarNotifChips.FLAG_NAME, PromotedNotificationUi.FLAG_NAME) + fun testIsSticky_promotedAndExpanded_notifChipsFlagOff_promotedUiFlagOff_true() { + assertThat(getIsSticky_promotedAndExpanded()).isTrue() + } - val headsUpEntry = underTest.getHeadsUpEntry(notifEntry.key) - headsUpEntry!!.setExpanded(true) + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME, PromotedNotificationUi.FLAG_NAME) + fun testIsSticky_promotedAndExpanded_notifChipsFlagOn_promotedUiFlagOn_false() { + assertThat(getIsSticky_promotedAndExpanded()).isFalse() + } - assertThat(underTest.isSticky(notifEntry.key)).isTrue() + @Test + @EnableFlags(PromotedNotificationUi.FLAG_NAME) + @DisableFlags(StatusBarNotifChips.FLAG_NAME) + fun testIsSticky_promotedAndExpanded_promotedUiFlagOn_false() { + assertThat(getIsSticky_promotedAndExpanded()).isFalse() } @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) + @DisableFlags(PromotedNotificationUi.FLAG_NAME) fun testIsSticky_promotedAndExpanded_notifChipsFlagOn_false() { + assertThat(getIsSticky_promotedAndExpanded()).isFalse() + } + + private fun getIsSticky_promotedAndExpanded(): Boolean { val notif = Notification.Builder(mContext, "").setSmallIcon(R.drawable.ic_person).build() notif.flags = FLAG_PROMOTED_ONGOING val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, notif) @@ -708,7 +716,7 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() { val headsUpEntry = underTest.getHeadsUpEntry(notifEntry.key) headsUpEntry!!.setExpanded(true) - assertThat(underTest.isSticky(notifEntry.key)).isFalse() + return underTest.isSticky(notifEntry.key) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt index 39cff63f363e..4f9beb6ffc73 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt @@ -6,6 +6,7 @@ import android.platform.test.flag.junit.FlagsParameterization import android.widget.FrameLayout import androidx.test.filters.SmallTest import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ShadeInterpolation.getContentAlpha import com.android.systemui.dump.DumpManager @@ -481,7 +482,11 @@ class StackScrollAlgorithmTest(flags: FlagsParameterization) : SysuiTestCase() { stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0) val marginBottom = - context.resources.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom) + context.resources.getDimensionPixelSize( + if (Flags.notificationsRedesignFooterView()) + R.dimen.notification_2025_panel_margin_bottom + else R.dimen.notification_panel_margin_bottom + ) val fullHeight = ambientState.layoutMaxHeight + marginBottom - ambientState.stackY val centeredY = ambientState.stackY + fullHeight / 2f - emptyShadeView.height / 2f assertThat(emptyShadeView.viewState.yTranslation).isEqualTo(centeredY) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt index 786b3590facb..77f02c050098 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -19,9 +19,12 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState +import com.android.systemui.Flags.FLAG_NOTIFICATIONS_REDESIGN_FOOTER_VIEW import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository import com.android.systemui.common.shared.model.NotificationContainerBounds @@ -298,6 +301,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S } @Test + @DisableFlags(FLAG_NOTIFICATIONS_REDESIGN_FOOTER_VIEW) fun validateMarginBottom() = testScope.runTest { overrideResource(R.dimen.notification_panel_margin_bottom, 50) @@ -310,6 +314,19 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S } @Test + @EnableFlags(FLAG_NOTIFICATIONS_REDESIGN_FOOTER_VIEW) + fun validateMarginBottom_footerRedesign() = + testScope.runTest { + overrideResource(R.dimen.notification_2025_panel_margin_bottom, 50) + + val dimens by collectLastValue(underTest.configurationBasedDimensions) + + configurationRepository.onAnyConfigurationChange() + + assertThat(dimens!!.marginBottom).isEqualTo(50) + } + + @Test @DisableSceneContainer fun validateMarginTopWithLargeScreenHeader_usesHelper() = testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt index b0b80a9419e2..52c41a07198d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt @@ -25,11 +25,14 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.kosmos.testScope import com.android.systemui.shared.Flags as SharedFlags import com.android.systemui.statusbar.SysuiStatusBarStateController +import com.android.systemui.testKosmos import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -48,6 +51,7 @@ class ActivityStarterImplTest : SysuiTestCase() { @Mock private lateinit var activityStarterInternal: ActivityStarterInternalImpl @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController private lateinit var underTest: ActivityStarterImpl + private val kosmos = testKosmos() private val mainExecutor = FakeExecutor(FakeSystemClock()) @Before @@ -69,12 +73,18 @@ class ActivityStarterImplTest : SysuiTestCase() { @EnableSceneContainer @Test fun registerTransition_forwardsTheRequest() { - val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java) - val controllerFactory = mock(ActivityTransitionAnimator.ControllerFactory::class.java) - - underTest.registerTransition(cookie, controllerFactory) - - verify(activityStarterInternal).registerTransition(eq(cookie), eq(controllerFactory)) + with(kosmos) { + testScope.runTest { + val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java) + val controllerFactory = + mock(ActivityTransitionAnimator.ControllerFactory::class.java) + + underTest.registerTransition(cookie, controllerFactory, testScope) + + verify(activityStarterInternal) + .registerTransition(eq(cookie), eq(controllerFactory), eq(testScope)) + } + } } @DisableFlags( @@ -83,12 +93,17 @@ class ActivityStarterImplTest : SysuiTestCase() { ) @Test fun registerTransition_doesNotForwardTheRequest_whenFlaggedOff() { - val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java) - val controllerFactory = mock(ActivityTransitionAnimator.ControllerFactory::class.java) + with(kosmos) { + testScope.runTest { + val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java) + val controllerFactory = + mock(ActivityTransitionAnimator.ControllerFactory::class.java) - underTest.registerTransition(cookie, controllerFactory) + underTest.registerTransition(cookie, controllerFactory, testScope) - verify(activityStarterInternal, never()).registerTransition(any(), any()) + verify(activityStarterInternal, never()).registerTransition(any(), any(), any()) + } + } } @EnableFlags( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt index 5406acf694ff..dfa5c9a26d79 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt @@ -42,6 +42,7 @@ import com.android.systemui.communal.domain.interactor.CommunalSettingsInteracto import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.ActivityStarter.OnDismissAction import com.android.systemui.settings.UserTracker import com.android.systemui.shade.ShadeController @@ -58,12 +59,14 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.window.StatusBarWindowController import com.android.systemui.statusbar.window.StatusBarWindowControllerStore +import com.android.systemui.testKosmos import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import java.util.Optional import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test @@ -109,6 +112,7 @@ class LegacyActivityStarterInternalImplTest : SysuiTestCase() { @Mock private lateinit var communalSceneInteractor: CommunalSceneInteractor @Mock private lateinit var communalSettingsInteractor: CommunalSettingsInteractor private lateinit var underTest: LegacyActivityStarterInternalImpl + private val kosmos = testKosmos() private val mainExecutor = FakeExecutor(FakeSystemClock()) private val shadeAnimationInteractor = ShadeAnimationInteractorLegacyImpl(ShadeAnimationRepository(), FakeShadeRepository()) @@ -157,13 +161,18 @@ class LegacyActivityStarterInternalImplTest : SysuiTestCase() { ) @Test fun registerTransition_registers() { - val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java) - val controllerFactory = mock(ActivityTransitionAnimator.ControllerFactory::class.java) - `when`(controllerFactory.cookie).thenReturn(cookie) + with(kosmos) { + testScope.runTest { + val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java) + val controllerFactory = + mock(ActivityTransitionAnimator.ControllerFactory::class.java) + `when`(controllerFactory.cookie).thenReturn(cookie) - underTest.registerTransition(cookie, controllerFactory) + underTest.registerTransition(cookie, controllerFactory, testScope) - verify(activityTransitionAnimator).register(eq(cookie), any()) + verify(activityTransitionAnimator).register(eq(cookie), any(), eq(testScope)) + } + } } @DisableFlags( @@ -172,14 +181,19 @@ class LegacyActivityStarterInternalImplTest : SysuiTestCase() { ) @Test fun registerTransition_throws_whenFlagsAreDisabled() { - val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java) - val controllerFactory = mock(ActivityTransitionAnimator.ControllerFactory::class.java) + with(kosmos) { + testScope.runTest { + val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java) + val controllerFactory = + mock(ActivityTransitionAnimator.ControllerFactory::class.java) - assertThrows(IllegalStateException::class.java) { - underTest.registerTransition(cookie, controllerFactory) - } + assertThrows(IllegalStateException::class.java) { + underTest.registerTransition(cookie, controllerFactory, testScope) + } - verify(activityTransitionAnimator, never()).register(any(), any()) + verify(activityTransitionAnimator, never()).register(any(), any(), any()) + } + } } @EnableFlags( diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java index ca98cbf20c3a..18891dba4b0d 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java @@ -25,6 +25,8 @@ import android.view.View; import com.android.systemui.animation.ActivityTransitionAnimator; import com.android.systemui.plugins.annotations.ProvidesInterface; +import kotlinx.coroutines.CoroutineScope; + /** * An interface to start activities. This is used as a callback from the views to * {@link PhoneStatusBar} to allow custom handling for starting the activity, i.e. dismissing the @@ -37,11 +39,12 @@ public interface ActivityStarter { /** * Registers the given {@link ActivityTransitionAnimator.ControllerFactory} for launching and * closing transitions matching the {@link ActivityTransitionAnimator.TransitionCookie} and the - * {@link ComponentName} that it contains. + * {@link ComponentName} that it contains, within the given {@link CoroutineScope}. */ void registerTransition( ActivityTransitionAnimator.TransitionCookie cookie, - ActivityTransitionAnimator.ControllerFactory controllerFactory); + ActivityTransitionAnimator.ControllerFactory controllerFactory, + CoroutineScope scope); /** * Unregisters the {@link ActivityTransitionAnimator.ControllerFactory} previously registered diff --git a/packages/SystemUI/pods/com/android/systemui/util/settings/UserSettingsProxy.kt b/packages/SystemUI/pods/com/android/systemui/util/settings/UserSettingsProxy.kt index 1a5517059ca4..68ad11e3ec01 100644 --- a/packages/SystemUI/pods/com/android/systemui/util/settings/UserSettingsProxy.kt +++ b/packages/SystemUI/pods/com/android/systemui/util/settings/UserSettingsProxy.kt @@ -157,7 +157,11 @@ interface UserSettingsProxy : SettingsProxy { userHandle: Int, ) = settingsScope.launch("registerContentObserverForUserAsync-A") { - registerContentObserverForUserSync(getUriFor(name), settingsObserver, userHandle) + try { + registerContentObserverForUserSync(getUriFor(name), settingsObserver, userHandle) + } catch (e: SecurityException) { + throw SecurityException("registerContentObserverForUserAsync-A, name: $name", e) + } } /** Convenience wrapper around [ContentResolver.registerContentObserver] */ @@ -198,7 +202,11 @@ interface UserSettingsProxy : SettingsProxy { userHandle: Int, ) = settingsScope.launch("registerContentObserverForUserAsync-B") { - registerContentObserverForUserSync(uri, settingsObserver, userHandle) + try { + registerContentObserverForUserSync(uri, settingsObserver, userHandle) + } catch (e: SecurityException) { + throw SecurityException("registerContentObserverForUserAsync-B, uri: $uri", e) + } } /** @@ -215,7 +223,11 @@ interface UserSettingsProxy : SettingsProxy { @WorkerThread registered: Runnable, ) = settingsScope.launch("registerContentObserverForUserAsync-C") { - registerContentObserverForUserSync(uri, settingsObserver, userHandle) + try { + registerContentObserverForUserSync(uri, settingsObserver, userHandle) + } catch (e: SecurityException) { + throw SecurityException("registerContentObserverForUserAsync-C, uri: $uri", e) + } registered.run() } @@ -274,12 +286,16 @@ interface UserSettingsProxy : SettingsProxy { userHandle: Int, ) { settingsScope.launch("registerContentObserverForUserAsync-D") { - registerContentObserverForUserSync( - getUriFor(name), - notifyForDescendants, - settingsObserver, - userHandle, - ) + try { + registerContentObserverForUserSync( + getUriFor(name), + notifyForDescendants, + settingsObserver, + userHandle, + ) + } catch (e: SecurityException) { + throw SecurityException("registerContentObserverForUserAsync-D, name: $name", e) + } } } @@ -338,12 +354,16 @@ interface UserSettingsProxy : SettingsProxy { userHandle: Int, ) = settingsScope.launch("registerContentObserverForUserAsync-E") { - registerContentObserverForUserSync( - uri, - notifyForDescendants, - settingsObserver, - userHandle, - ) + try { + registerContentObserverForUserSync( + uri, + notifyForDescendants, + settingsObserver, + userHandle, + ) + } catch (e: SecurityException) { + throw SecurityException("registerContentObserverForUserAsync-E, uri: $uri", e) + } } /** diff --git a/packages/SystemUI/res/layout/notification_stack_scroll_layout.xml b/packages/SystemUI/res/layout/notification_stack_scroll_layout.xml index 65cf81ea416b..5954de4012c8 100644 --- a/packages/SystemUI/res/layout/notification_stack_scroll_layout.xml +++ b/packages/SystemUI/res/layout/notification_stack_scroll_layout.xml @@ -16,6 +16,7 @@ --> <!-- This XML is served to be overridden by other OEMs/device types. --> +<!-- Note: The margins may be overridden in code, see NotificationStackScrollLayout#getBottomMargin --> <com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:systemui="http://schemas.android.com/apk/res-auto" diff --git a/packages/SystemUI/res/values-sw600dp/config.xml b/packages/SystemUI/res/values-sw600dp/config.xml index ab0f788dbb13..b4383156dc71 100644 --- a/packages/SystemUI/res/values-sw600dp/config.xml +++ b/packages/SystemUI/res/values-sw600dp/config.xml @@ -19,7 +19,7 @@ <!-- These resources are around just to allow their values to be customized for different hardware and product builds. --> -<resources xmlns:android="http://schemas.android.com/apk/res/android"> +<resources> <!-- The maximum number of rows in the QuickSettings --> <integer name="quick_settings_max_rows">4</integer> @@ -51,9 +51,7 @@ ignored. --> <string-array name="config_keyguardQuickAffordanceDefaults" translatable="false"> <item>bottom_start:home</item> - <!-- TODO(b/384119565): revisit decision on defaults --> - <item android:featureFlag="!com.android.systemui.glanceable_hub_v2_resources">bottom_end:create_note</item> - <item android:featureFlag="com.android.systemui.glanceable_hub_v2_resources">bottom_end:glanceable_hub</item> + <item>bottom_end:create_note</item> </string-array> <!-- Whether volume panel should use the large screen layout or not --> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 42d66e23feb9..a96ebe7b4fd6 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -574,6 +574,8 @@ <dimen name="notification_panel_margin_bottom">32dp</dimen> + <dimen name="notification_2025_panel_margin_bottom">64dp</dimen> + <!-- The bottom padding of the panel that holds the list of notifications. --> <dimen name="notification_panel_padding_bottom">0dp</dimen> @@ -1781,6 +1783,7 @@ <dimen name="wallet_button_vertical_padding">8dp</dimen> <!-- Ongoing activity chip --> + <dimen name="ongoing_activity_chip_min_text_width">12dp</dimen> <dimen name="ongoing_activity_chip_max_text_width">74dp</dimen> <dimen name="ongoing_activity_chip_margin_start">5dp</dimen> <!-- The activity chip side padding, used with the default phone icon. --> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index c266a5b47cff..0b66a0ffb711 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -296,7 +296,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private final Provider<JavaAdapter> mJavaAdapter; private final Provider<SceneInteractor> mSceneInteractor; private final Provider<AlternateBouncerInteractor> mAlternateBouncerInteractor; - private final CommunalSceneInteractor mCommunalSceneInteractor; + private final Provider<CommunalSceneInteractor> mCommunalSceneInteractor; private final AuthController mAuthController; private final UiEventLogger mUiEventLogger; private final Set<String> mAllowFingerprintOnOccludingActivitiesFromPackage; @@ -2210,7 +2210,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab Provider<AlternateBouncerInteractor> alternateBouncerInteractor, Provider<JavaAdapter> javaAdapter, Provider<SceneInteractor> sceneInteractor, - CommunalSceneInteractor communalSceneInteractor) { + Provider<CommunalSceneInteractor> communalSceneInteractor) { mContext = context; mSubscriptionManager = subscriptionManager; mUserTracker = userTracker; @@ -2543,7 +2543,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab if (glanceableHubV2()) { mJavaAdapter.get().alwaysCollectFlow( - mCommunalSceneInteractor.isCommunalVisible(), + mCommunalSceneInteractor.get().isCommunalVisible(), this::onCommunalShowingChanged ); } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationAnimationController.java index 615363da073a..db2ca1dbff02 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationAnimationController.java @@ -203,8 +203,8 @@ class WindowMagnificationAnimationController implements ValueAnimator.AnimatorUp return; } final float currentScale = mController.getScale(); - final float currentCenterX = mController.getCenterX(); - final float currentCenterY = mController.getCenterY(); + final float currentCenterX = mController.getMagnificationFrameCenterX(); + final float currentCenterY = mController.getMagnificationFrameCenterY(); if (mState == STATE_DISABLED) { // We don't need to offset the center during the animation. diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java index 1587ab16fc38..a67ec65cceda 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java @@ -497,6 +497,9 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold if (configDiff == 0) { return; } + if (Flags.updateWindowMagnifierBottomBoundary()) { + updateSystemGestureInsetsTop(); + } if ((configDiff & ActivityInfo.CONFIG_ORIENTATION) != 0) { onRotate(); } @@ -542,8 +545,11 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold } mWindowBounds.set(currentWindowBounds); final Size windowFrameSize = restoreMagnificationWindowFrameIndexAndSizeIfPossible(); - final float newCenterX = (getCenterX()) * mWindowBounds.width() / oldWindowBounds.width(); - final float newCenterY = (getCenterY()) * mWindowBounds.height() / oldWindowBounds.height(); + final float newCenterX = + (getMagnificationFrameCenterX()) * mWindowBounds.width() / oldWindowBounds.width(); + final float newCenterY = + (getMagnificationFrameCenterY()) * mWindowBounds.height() + / oldWindowBounds.height(); setMagnificationFrame(windowFrameSize.getWidth(), windowFrameSize.getHeight(), (int) newCenterX, (int) newCenterY); @@ -672,8 +678,12 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold } private void onWindowInsetChanged() { - if (updateSystemGestureInsetsTop()) { - updateSystemUIStateIfNeeded(); + if (Flags.updateWindowMagnifierBottomBoundary()) { + updateSystemGestureInsetsTop(); + } else { + if (updateSystemGestureInsetsTop()) { + updateSystemUIStateIfNeeded(); + } } } @@ -939,7 +949,9 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold final int x = MathUtils.clamp(mMagnificationFrame.left - mMirrorSurfaceMargin, minX, maxX); final int minY = -mOuterBorderSize; - final int maxY = mWindowBounds.bottom - height + mOuterBorderSize; + final int maxY = Flags.updateWindowMagnifierBottomBoundary() + ? mSystemGestureTop - height + mOuterBorderSize + : mWindowBounds.bottom - height + mOuterBorderSize; final int y = MathUtils.clamp(mMagnificationFrame.top - mMirrorSurfaceMargin, minY, maxY); if (computeWindowSize) { @@ -1098,6 +1110,10 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold } private void updateSysUIState(boolean force) { + if (Flags.updateWindowMagnifierBottomBoundary()) { + return; + } + final boolean overlap = isActivated() && mSystemGestureTop > 0 && mMirrorViewBounds.bottom > mSystemGestureTop; if (force || overlap != mOverlapWithGestureInsets) { @@ -1313,7 +1329,7 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold * * @return the X coordinate. {@link Float#NaN} if the window is invisible. */ - float getCenterX() { + float getMagnificationFrameCenterX() { return isActivated() ? mMagnificationFrame.exactCenterX() : Float.NaN; } @@ -1322,10 +1338,30 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold * * @return the Y coordinate. {@link Float#NaN} if the window is invisible. */ - float getCenterY() { + float getMagnificationFrameCenterY() { return isActivated() ? mMagnificationFrame.exactCenterY() : Float.NaN; } + /** + * Returns the screen-relative X coordinate of the center of the magnifier window. + * This could be different from the position of the magnification frame since the magnification + * frame could overlap with the bottom inset, but the magnifier window would not. + * @return the Y coordinate. {@link Float#NaN} if the window is invisible. + */ + float getMagnifierWindowX() { + return isActivated() ? (float) mMirrorViewBounds.left : Float.NaN; + } + + /** + * Returns the screen-relative Y coordinate of the center of the magnifier window. + * This could be different from the position of the magnification frame since the magnification + * frame could overlap with the bottom inset, but the magnifier window would not. + * @return the Y coordinate. {@link Float#NaN} if the window is invisible. + */ + float getMagnifierWindowY() { + return isActivated() ? (float) mMirrorViewBounds.top : Float.NaN; + } + @VisibleForTesting boolean isDiagonalScrollingEnabled() { diff --git a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt index 6f2a2c4ccaaa..b13f6df3f4f5 100644 --- a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt +++ b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt @@ -16,49 +16,75 @@ package com.android.systemui.brightness.ui.compose +import android.content.Context import android.view.MotionEvent +import androidx.annotation.VisibleForTesting +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.animation.core.VectorConverter import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.interaction.DragInteraction import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.height import androidx.compose.foundation.shape.CornerSize +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.painter.ColorPainter +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.input.pointer.pointerInteropFilter import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.app.tracing.coroutines.launchTraced as launch -import com.android.compose.PlatformSlider import com.android.compose.ui.graphics.drawInOverlay import com.android.systemui.Flags +import com.android.systemui.biometrics.Utils.toBitmap import com.android.systemui.brightness.shared.model.GammaBrightness +import com.android.systemui.brightness.ui.compose.AnimationSpecs.IconAppearSpec +import com.android.systemui.brightness.ui.compose.AnimationSpecs.IconDisappearSpec +import com.android.systemui.brightness.ui.compose.Dimensions.IconPadding +import com.android.systemui.brightness.ui.compose.Dimensions.IconSize +import com.android.systemui.brightness.ui.compose.Dimensions.SliderBackgroundFrameSize +import com.android.systemui.brightness.ui.compose.Dimensions.SliderBackgroundRoundedCorner +import com.android.systemui.brightness.ui.compose.Dimensions.SliderTrackRoundedCorner +import com.android.systemui.brightness.ui.compose.Dimensions.ThumbTrackGapSize import com.android.systemui.brightness.ui.viewmodel.BrightnessSliderViewModel import com.android.systemui.brightness.ui.viewmodel.Drag import com.android.systemui.common.shared.model.Icon -import com.android.systemui.common.shared.model.Text -import com.android.systemui.common.ui.compose.Icon import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.haptics.slider.SeekableSliderTrackerConfig import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig @@ -67,27 +93,32 @@ import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.qs.ui.compose.borderOnFocus import com.android.systemui.res.R import com.android.systemui.utils.PolicyRestriction +import platform.test.motion.compose.values.MotionTestValueKey +import platform.test.motion.compose.values.motionTestValues +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable -private fun BrightnessSlider( - viewModel: BrightnessSliderViewModel, +@VisibleForTesting +fun BrightnessSlider( gammaValue: Int, valueRange: IntRange, - label: Text.Resource, - icon: Icon, + iconResProvider: (Float) -> Int, + imageLoader: suspend (Int, Context) -> Icon.Loaded, restriction: PolicyRestriction, onRestrictedClick: (PolicyRestriction.Restricted) -> Unit, onDrag: (Int) -> Unit, onStop: (Int) -> Unit, + overriddenByAppState: Boolean, modifier: Modifier = Modifier, - formatter: (Int) -> String = { "$it" }, + showToast: () -> Unit = {}, hapticsViewModelFactory: SliderHapticsViewModel.Factory, ) { var value by remember(gammaValue) { mutableIntStateOf(gammaValue) } val animatedValue by animateFloatAsState(targetValue = value.toFloat(), label = "BrightnessSliderAnimatedValue") val floatValueRange = valueRange.first.toFloat()..valueRange.last.toFloat() - val isRestricted = remember(restriction) { restriction is PolicyRestriction.Restricted } + val isRestricted = restriction is PolicyRestriction.Restricted + val enabled = !isRestricted val interactionSource = remember { MutableInteractionSource() } val hapticsViewModel: SliderHapticsViewModel? = if (Flags.hapticsForComposeSliders()) { @@ -105,20 +136,56 @@ private fun BrightnessSlider( } else { null } + val colors = SliderDefaults.colors() - val overriddenByAppState by - if (Flags.showToastWhenAppControlBrightness()) { - viewModel.brightnessOverriddenByWindow.collectAsStateWithLifecycle() - } else { - remember { mutableStateOf(false) } + // The value state is recreated every time gammaValue changes, so we recreate this derivedState + // We have to use value as that's the value that changes when the user is dragging (gammaValue + // is always the starting value: actual (not temporary) brightness). + val iconRes by + remember(gammaValue, valueRange) { + derivedStateOf { + val percentage = + (value - valueRange.first) * 100f / (valueRange.last - valueRange.first) + iconResProvider(percentage) + } + } + val context = LocalContext.current + val painter: Painter by + produceState<Painter>( + initialValue = ColorPainter(Color.Transparent), + key1 = iconRes, + key2 = context, + ) { + val icon = imageLoader(iconRes, context) + // toBitmap is Drawable?.() -> Bitmap? and handles null internally. + val bitmap = icon.drawable.toBitmap()!!.asImageBitmap() + this@produceState.value = BitmapPainter(bitmap) + } + + val activeIconColor = colors.activeTickColor + val inactiveIconColor = colors.inactiveTickColor + val trackIcon: DrawScope.(Offset, Color, Float) -> Unit = + remember(painter) { + { offset, color, alpha -> + translate(offset.x + IconPadding.toPx(), offset.y) { + with(painter) { + draw( + IconSize.toSize(), + colorFilter = ColorFilter.tint(color), + alpha = alpha, + ) + } + } + } } - PlatformSlider( + Slider( value = animatedValue, valueRange = floatValueRange, - enabled = !isRestricted, + enabled = enabled, + colors = colors, onValueChange = { - if (!isRestricted) { + if (enabled) { if (!overriddenByAppState) { hapticsViewModel?.onValueChange(it) value = it.toInt() @@ -127,7 +194,7 @@ private fun BrightnessSlider( } }, onValueChangeFinished = { - if (!isRestricted) { + if (enabled) { if (!overriddenByAppState) { hapticsViewModel?.onValueChangeEnded() onStop(value) @@ -140,46 +207,117 @@ private fun BrightnessSlider( onRestrictedClick(restriction) } }, - icon = { isDragging -> - if (isDragging) { - Text(text = formatter(value)) - } else { - Icon(modifier = Modifier.size(24.dp), icon = icon) - } + interactionSource = interactionSource, + thumb = { + SliderDefaults.Thumb( + interactionSource = interactionSource, + enabled = enabled, + thumbSize = DpSize(4.dp, 52.dp), + ) }, - label = { - Text( - text = stringResource(id = label.res), - style = MaterialTheme.typography.titleMedium, - maxLines = 1, + track = { sliderState -> + var showIconActive by remember { mutableStateOf(true) } + val iconActiveAlphaAnimatable = remember { + Animatable( + initialValue = 1f, + typeConverter = Float.VectorConverter, + label = "iconActiveAlpha", + ) + } + + val iconInactiveAlphaAnimatable = remember { + Animatable( + initialValue = 0f, + typeConverter = Float.VectorConverter, + label = "iconInactiveAlpha", + ) + } + + LaunchedEffect(iconActiveAlphaAnimatable, iconInactiveAlphaAnimatable, showIconActive) { + if (showIconActive) { + launch { iconActiveAlphaAnimatable.appear() } + launch { iconInactiveAlphaAnimatable.disappear() } + } else { + launch { iconActiveAlphaAnimatable.disappear() } + launch { iconInactiveAlphaAnimatable.appear() } + } + } + + SliderDefaults.Track( + sliderState = sliderState, + modifier = + Modifier.motionTestValues { + (iconActiveAlphaAnimatable.isRunning || + iconInactiveAlphaAnimatable.isRunning) exportAs + BrightnessSliderMotionTestKeys.AnimatingIcon + + iconActiveAlphaAnimatable.value exportAs + BrightnessSliderMotionTestKeys.ActiveIconAlpha + iconInactiveAlphaAnimatable.value exportAs + BrightnessSliderMotionTestKeys.InactiveIconAlpha + } + .height(40.dp) + .drawWithContent { + drawContent() + + val yOffset = size.height / 2 - IconSize.toSize().height / 2 + val activeTrackStart = 0f + val activeTrackEnd = + size.width * sliderState.coercedValueAsFraction - + ThumbTrackGapSize.toPx() + val inactiveTrackStart = activeTrackEnd + ThumbTrackGapSize.toPx() * 2 + val inactiveTrackEnd = size.width + + val activeTrackWidth = activeTrackEnd - activeTrackStart + val inactiveTrackWidth = inactiveTrackEnd - inactiveTrackStart + if ( + IconSize.toSize().width < activeTrackWidth - IconPadding.toPx() * 2 + ) { + showIconActive = true + trackIcon( + Offset(activeTrackStart, yOffset), + activeIconColor, + iconActiveAlphaAnimatable.value, + ) + } else if ( + IconSize.toSize().width < + inactiveTrackWidth - IconPadding.toPx() * 2 + ) { + showIconActive = false + trackIcon( + Offset(inactiveTrackStart, yOffset), + inactiveIconColor, + iconInactiveAlphaAnimatable.value, + ) + } + }, + trackCornerSize = SliderTrackRoundedCorner, + trackInsideCornerSize = 2.dp, + drawStopIndicator = null, + thumbTrackGapSize = ThumbTrackGapSize, ) }, - interactionSource = interactionSource, ) + + val currentShowToast by rememberUpdatedState(showToast) // Showing the warning toast if the current running app window has controlled the // brightness value. if (Flags.showToastWhenAppControlBrightness()) { - val context = LocalContext.current LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> if (interaction is DragInteraction.Start && overriddenByAppState) { - viewModel.showToast( - context, - R.string.quick_settings_brightness_unable_adjust_msg, - ) + currentShowToast() } } } } } -private val sliderBackgroundFrameSize = 8.dp - private fun Modifier.sliderBackground(color: Color) = drawWithCache { - val offsetAround = sliderBackgroundFrameSize.toPx() - val newSize = Size(size.width + 2 * offsetAround, size.height + 2 * offsetAround) - val offset = Offset(-offsetAround, -offsetAround) - val cornerRadius = CornerRadius(offsetAround + size.height / 2) + val offsetAround = SliderBackgroundFrameSize.toSize() + val newSize = Size(size.width + 2 * offsetAround.width, size.height + 2 * offsetAround.height) + val offset = Offset(-offsetAround.width, -offsetAround.height) + val cornerRadius = CornerRadius(SliderBackgroundRoundedCorner.toPx()) onDrawBehind { drawRoundRect(color = color, topLeft = offset, size = newSize, cornerRadius = cornerRadius) } @@ -192,21 +330,30 @@ fun BrightnessSliderContainer( containerColor: Color = colorResource(R.color.shade_scrim_background_dark), ) { val gamma = viewModel.currentBrightness.value + if (gamma == BrightnessSliderViewModel.initialValue.value) { // Ignore initial negative value. + return + } + val context = LocalContext.current val coroutineScope = rememberCoroutineScope() val restriction by viewModel.policyRestriction.collectAsStateWithLifecycle( initialValue = PolicyRestriction.NoRestriction ) + val overriddenByAppState by + if (Flags.showToastWhenAppControlBrightness()) { + viewModel.brightnessOverriddenByWindow.collectAsStateWithLifecycle() + } else { + remember { mutableStateOf(false) } + } DisposableEffect(Unit) { onDispose { viewModel.setIsDragging(false) } } Box(modifier = modifier.fillMaxWidth().sysuiResTag("brightness_slider")) { BrightnessSlider( - viewModel = viewModel, gammaValue = gamma, valueRange = viewModel.minBrightness.value..viewModel.maxBrightness.value, - label = viewModel.label, - icon = viewModel.icon, + iconResProvider = BrightnessSliderViewModel::getIconForPercentage, + imageLoader = viewModel::loadImage, restriction = restriction, onRestrictedClick = viewModel::showPolicyRestrictionDialog, onDrag = { @@ -220,7 +367,7 @@ fun BrightnessSliderContainer( modifier = Modifier.borderOnFocus( color = MaterialTheme.colorScheme.secondary, - cornerSize = CornerSize(32.dp), + cornerSize = CornerSize(SliderTrackRoundedCorner), ) .then(if (viewModel.showMirror) Modifier.drawInOverlay() else Modifier) .sliderBackground(containerColor) @@ -234,8 +381,38 @@ fun BrightnessSliderContainer( } false }, - formatter = viewModel::formatValue, hapticsViewModelFactory = viewModel.hapticsViewModelFactory, + overriddenByAppState = overriddenByAppState, + showToast = { + viewModel.showToast(context, R.string.quick_settings_brightness_unable_adjust_msg) + }, ) } } + +private object Dimensions { + val SliderBackgroundFrameSize = DpSize(10.dp, 6.dp) + val SliderBackgroundRoundedCorner = 24.dp + val SliderTrackRoundedCorner = 12.dp + val IconSize = DpSize(28.dp, 28.dp) + val IconPadding = 6.dp + val ThumbTrackGapSize = 6.dp +} + +private object AnimationSpecs { + val IconAppearSpec = tween<Float>(durationMillis = 100, delayMillis = 33) + val IconDisappearSpec = tween<Float>(durationMillis = 50) +} + +private suspend fun Animatable<Float, AnimationVector1D>.appear() = + animateTo(targetValue = 1f, animationSpec = IconAppearSpec) + +private suspend fun Animatable<Float, AnimationVector1D>.disappear() = + animateTo(targetValue = 0f, animationSpec = IconDisappearSpec) + +@VisibleForTesting +object BrightnessSliderMotionTestKeys { + val AnimatingIcon = MotionTestValueKey<Boolean>("animatingIcon") + val ActiveIconAlpha = MotionTestValueKey<Float>("activeIconAlpha") + val InactiveIconAlpha = MotionTestValueKey<Float>("inactiveIconAlpha") +} diff --git a/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt index 7df71550d43d..ed1cef3fccaf 100644 --- a/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt @@ -17,6 +17,8 @@ package com.android.systemui.brightness.ui.viewmodel import android.content.Context +import androidx.annotation.DrawableRes +import androidx.annotation.FloatRange import androidx.annotation.StringRes import androidx.compose.runtime.getValue import com.android.systemui.brightness.domain.interactor.BrightnessPolicyEnforcementInteractor @@ -24,9 +26,9 @@ import com.android.systemui.brightness.domain.interactor.ScreenBrightnessInterac import com.android.systemui.brightness.shared.model.GammaBrightness import com.android.systemui.classifier.Classifier import com.android.systemui.classifier.domain.interactor.FalsingInteractor -import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon -import com.android.systemui.common.shared.model.Text +import com.android.systemui.common.shared.model.asIcon +import com.android.systemui.graphics.ImageLoader import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator @@ -57,6 +59,7 @@ constructor( private val falsingInteractor: FalsingInteractor, @Assisted private val supportsMirroring: Boolean, private val brightnessWarningToast: BrightnessWarningToast, + private val imageLoader: ImageLoader, ) : ExclusiveActivatable() { private val hydrator = Hydrator("BrightnessSliderViewModel.hydrator") @@ -64,17 +67,13 @@ constructor( val currentBrightness by hydrator.hydratedStateOf( "currentBrightness", - GammaBrightness(0), + initialValue, screenBrightnessInteractor.gammaBrightness, ) val maxBrightness = screenBrightnessInteractor.maxGammaBrightness val minBrightness = screenBrightnessInteractor.minGammaBrightness - val label = Text.Resource(R.string.quick_settings_brightness_dialog_title) - - val icon = Icon.Resource(R.drawable.ic_brightness_full, ContentDescription.Resource(label.res)) - val policyRestriction = brightnessPolicyEnforcementInteractor.brightnessPolicyRestriction fun showPolicyRestrictionDialog(restriction: PolicyRestriction.Restricted) { @@ -94,6 +93,16 @@ constructor( falsingInteractor.isFalseTouch(Classifier.BRIGHTNESS_SLIDER) } + suspend fun loadImage(@DrawableRes resId: Int, context: Context): Icon.Loaded { + return imageLoader + .loadDrawable( + android.graphics.drawable.Icon.createWithResource(context, resId), + maxHeight = 200, + maxWidth = 200, + )!! + .asIcon(null, resId) + } + /** * As a brightness slider is dragged, the corresponding events should be sent using this method. */ @@ -104,18 +113,6 @@ constructor( } } - /** - * Format the current value of brightness as a percentage between the minimum and maximum gamma. - */ - fun formatValue(value: Int): String { - val min = minBrightness.value - val max = maxBrightness.value - val coercedValue = value.coerceIn(min, max) - val percentage = (coercedValue - min) * 100 / (max - min) - // This is not finalized UI so using fixed string - return "$percentage%" - } - fun setIsDragging(dragging: Boolean) { brightnessMirrorShowingInteractor.setMirrorShowing(dragging && supportsMirroring) } @@ -131,6 +128,26 @@ constructor( interface Factory { fun create(supportsMirroring: Boolean): BrightnessSliderViewModel } + + companion object { + val initialValue = GammaBrightness(-1) + + private val icons = + BrightnessIcons( + brightnessLow = R.drawable.ic_brightness_low, + brightnessMid = R.drawable.ic_brightness_medium, + brightnessHigh = R.drawable.ic_brightness_full, + ) + + @DrawableRes + fun getIconForPercentage(@FloatRange(0.0, 100.0) percentage: Float): Int { + return when { + percentage <= 20f -> icons.brightnessLow + percentage >= 80f -> icons.brightnessHigh + else -> icons.brightnessMid + } + } + } } fun BrightnessSliderViewModel.Factory.create() = create(supportsMirroring = true) @@ -143,3 +160,9 @@ sealed interface Drag { @JvmInline value class Stopped(override val brightness: GammaBrightness) : Drag } + +private data class BrightnessIcons( + @DrawableRes val brightnessLow: Int, + @DrawableRes val brightnessMid: Int, + @DrawableRes val brightnessHigh: Int, +) diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt index 643d185f1939..8b6322720118 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt @@ -16,7 +16,9 @@ package com.android.systemui.communal.data.repository +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.compose.animation.scene.ObservableTransitionState +import com.android.compose.animation.scene.OverlayKey import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.TransitionKey import com.android.systemui.communal.dagger.Communal @@ -25,16 +27,17 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.scene.shared.model.SceneDataSource +import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn -import com.android.app.tracing.coroutines.launchTraced as launch /** Encapsulates the state of communal mode. */ interface CommunalSceneRepository { @@ -52,6 +55,9 @@ interface CommunalSceneRepository { /** Immediately snaps to the desired scene. */ fun snapToScene(toScene: SceneKey) + /** Shows the hub from a power button press. */ + suspend fun showHubFromPowerButton() + /** * Updates the transition state of the hub [SceneTransitionLayout]. * @@ -67,6 +73,7 @@ constructor( @Application private val applicationScope: CoroutineScope, @Background backgroundScope: CoroutineScope, @Communal private val sceneDataSource: SceneDataSource, + @Communal private val delegator: SceneDataSourceDelegator, ) : CommunalSceneRepository { override val currentScene: StateFlow<SceneKey> = sceneDataSource.currentScene @@ -98,6 +105,18 @@ constructor( } } + override suspend fun showHubFromPowerButton() { + // If keyguard is not showing yet, the hub view is not ready and the + // [SceneDataSourceDelegator] will still be using the default [NoOpSceneDataSource] + // and initial key, which is Blank. This means that when the hub container loads, it + // will default to not showing the hub. Attempting to set the scene in this state + // is simply ignored by the [NoOpSceneDataSource]. Instead, we temporarily override + // it with a new one that defaults to Communal. This delegate will be overwritten + // once the [CommunalContainer] loads. + // TODO(b/392969914): show the hub first instead of forcing the scene. + delegator.setDelegate(NoOpSceneDataSource(CommunalScenes.Communal)) + } + /** * Updates the transition state of the hub [SceneTransitionLayout]. * @@ -106,4 +125,27 @@ constructor( override fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) { _transitionState.value = transitionState } + + /** Noop implementation of a scene data source that always returns the initial [SceneKey]. */ + private class NoOpSceneDataSource(initialSceneKey: SceneKey) : SceneDataSource { + override val currentScene: StateFlow<SceneKey> = + MutableStateFlow(initialSceneKey).asStateFlow() + + override val currentOverlays: StateFlow<Set<OverlayKey>> = + MutableStateFlow(emptySet<OverlayKey>()).asStateFlow() + + override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) = Unit + + override fun snapToScene(toScene: SceneKey) = Unit + + override fun showOverlay(overlay: OverlayKey, transitionKey: TransitionKey?) = Unit + + override fun hideOverlay(overlay: OverlayKey, transitionKey: TransitionKey?) = Unit + + override fun replaceOverlay( + from: OverlayKey, + to: OverlayKey, + transitionKey: TransitionKey?, + ) = Unit + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt index 476493225857..3d9e93036dbc 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt @@ -148,6 +148,29 @@ constructor( } } + fun showHubFromPowerButton() { + val loggingReason = "showing hub from power button" + applicationScope.launch("$TAG#showHubFromPowerButton") { + if (SceneContainerFlag.isEnabled) { + sceneInteractor.changeScene( + toScene = CommunalScenes.Communal.toSceneContainerSceneKey(), + loggingReason = loggingReason, + ) + return@launch + } + + if (currentScene.value == CommunalScenes.Communal) return@launch + logger.logSceneChangeRequested( + from = currentScene.value, + to = CommunalScenes.Communal, + reason = loggingReason, + isInstant = true, + ) + notifyListeners(CommunalScenes.Communal, null) + repository.showHubFromPowerButton() + } + } + private fun notifyListeners(newScene: SceneKey, keyguardState: KeyguardState?) { onSceneAboutToChangeListener.forEach { it.onSceneAboutToChange(newScene, keyguardState) } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt index 099a85926020..49003a735fbd 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt @@ -45,7 +45,7 @@ abstract class BaseCommunalViewModel( val mediaHost: MediaHost, val mediaCarouselController: MediaCarouselController, ) { - val currentScene: Flow<SceneKey> = communalSceneInteractor.currentScene + val currentScene: StateFlow<SceneKey> = communalSceneInteractor.currentScene /** Used to animate showing or hiding the communal content. */ open val isCommunalContentVisible: Flow<Boolean> = MutableStateFlow(false) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt index dd2bec143292..0f5f31302670 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt @@ -55,6 +55,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter @@ -289,7 +290,7 @@ constructor( } private val areBiometricsEnabledForDeviceEntryFromUserSetting: Flow<Triple<Int, Boolean, Int>> = - conflatedCallbackFlow { + callbackFlow { val callback = object : IBiometricEnabledOnKeyguardCallback.Stub() { override fun onChanged(enabled: Boolean, userId: Int, modality: Int) { diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt index ece97bd27df7..9e32dd8d74ae 100644 --- a/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt +++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt @@ -29,6 +29,7 @@ import java.io.PrintWriter import java.util.Optional import javax.inject.Inject import javax.inject.Named +import javax.inject.Provider /** * Monitors ambient light signals, applies a debouncing algorithm, and produces the current ambient @@ -43,7 +44,7 @@ class AmbientLightModeMonitor constructor( private val algorithm: Optional<DebounceAlgorithm>, private val sensorManager: AsyncSensorManager, - @Named(LIGHT_SENSOR) private val lightSensor: Optional<Sensor>, + @Named(LIGHT_SENSOR) private val lightSensor: Optional<Provider<Sensor>>, ) : Dumpable { companion object { private const val TAG = "AmbientLightModeMonitor" @@ -67,7 +68,7 @@ constructor( fun start(callback: Callback) { if (DEBUG) Log.d(TAG, "start monitoring ambient light mode") - if (lightSensor.isEmpty) { + if (lightSensor.isEmpty || lightSensor.get().get() == null) { if (DEBUG) Log.w(TAG, "light sensor not available") return } @@ -80,7 +81,7 @@ constructor( algorithm.get().start(callback) sensorManager.registerListener( mSensorEventListener, - lightSensor.get(), + lightSensor.get().get(), SensorManager.SENSOR_DELAY_NORMAL, ) } diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java index c08be51c0699..8469cb4ab565 100644 --- a/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java +++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java @@ -16,6 +16,7 @@ package com.android.systemui.lowlightclock.dagger; +import android.annotation.Nullable; import android.content.res.Resources; import android.hardware.Sensor; @@ -100,6 +101,7 @@ public abstract class LowLightModule { abstract LowLightDisplayController bindsLowLightDisplayController(); @BindsOptionalOf + @Nullable @Named(LIGHT_SENSOR) abstract Sensor bindsLightSensor(); diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java index 52b3c3ecacc6..b391cb079ec5 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java @@ -156,6 +156,34 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { boolean isMutingExpectedDeviceExist = mController.hasMutingExpectedDevice(); final boolean currentlyConnected = isCurrentlyConnected(device); boolean isCurrentSeekbarInvisible = mSeekBar.getVisibility() == View.GONE; + boolean isSelected = isDeviceIncluded(mController.getSelectedMediaDevice(), device); + boolean isDeselectable = + isDeviceIncluded(mController.getDeselectableMediaDevice(), device); + boolean isSelectable = isDeviceIncluded(mController.getSelectableMediaDevice(), device); + boolean isTransferable = + isDeviceIncluded(mController.getTransferableMediaDevices(), device); + boolean hasRouteListingPreferenceItem = device.hasRouteListingPreferenceItem(); + + if (DEBUG) { + Log.d( + TAG, + "[" + + position + + "] " + + device.getName() + + " [" + + (isDeselectable ? "deselectable" : "") + + "] [" + + (isSelected ? "selected" : "") + + "] [" + + (isSelectable ? "selectable" : "") + + "] [" + + (isTransferable ? "transferable" : "") + + "] [" + + (hasRouteListingPreferenceItem ? "hasListingPreference" : "") + + "]"); + } + if (mCurrentActivePosition == position) { mCurrentActivePosition = -1; } @@ -210,8 +238,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { } } else if (device.hasSubtext()) { boolean isActiveWithOngoingSession = - (device.hasOngoingSession() && (currentlyConnected || isDeviceIncluded( - mController.getSelectedMediaDevice(), device))); + device.hasOngoingSession() && (currentlyConnected || isSelected); boolean isHost = device.isHostForOngoingSession() && isActiveWithOngoingSession; if (isActiveWithOngoingSession) { @@ -266,16 +293,13 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { setSingleLineLayout(device.getName(), false /* showSeekBar*/, true /* showProgressBar */, false /* showCheckBox */, false /* showEndTouchArea */); - } else if (mController.getSelectedMediaDevice().size() > 1 - && isDeviceIncluded(mController.getSelectedMediaDevice(), device)) { + } else if (mController.getSelectedMediaDevice().size() > 1 && isSelected) { // selected device in group - boolean isDeviceDeselectable = isDeviceIncluded( - mController.getDeselectableMediaDevice(), device); - boolean showEndArea = !Flags.enableOutputSwitcherSessionGrouping() - || isDeviceDeselectable; + boolean showEndArea = + !Flags.enableOutputSwitcherSessionGrouping() || isDeselectable; updateUnmutedVolumeIcon(device); - updateGroupableCheckBox(true, isDeviceDeselectable, device); - updateEndClickArea(device, isDeviceDeselectable); + updateGroupableCheckBox(true, isDeselectable, device); + updateEndClickArea(device, isDeselectable); disableFocusPropertyForView(mContainerLayout); setUpContentDescriptionForView(mSeekBar, device); setSingleLineLayout(device.getName(), true /* showSeekBar */, @@ -307,10 +331,8 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { //If device is connected and there's other selectable devices, layout as // one of selected devices. updateUnmutedVolumeIcon(device); - boolean isDeviceDeselectable = isDeviceIncluded( - mController.getDeselectableMediaDevice(), device); - updateGroupableCheckBox(true, isDeviceDeselectable, device); - updateEndClickArea(device, isDeviceDeselectable); + updateGroupableCheckBox(true, isDeselectable, device); + updateEndClickArea(device, isDeselectable); disableFocusPropertyForView(mContainerLayout); setUpContentDescriptionForView(mSeekBar, device); setSingleLineLayout(device.getName(), true /* showSeekBar */, @@ -327,12 +349,16 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { false /* showEndTouchArea */); initSeekbar(device, isCurrentSeekbarInvisible); } - } else if (isDeviceIncluded(mController.getSelectableMediaDevice(), device)) { + } else if (isSelectable) { //groupable device setUpDeviceIcon(device); updateGroupableCheckBox(false, true, device); updateEndClickArea(device, true); - updateFullItemClickListener(v -> onItemClick(v, device)); + if (!Flags.disableTransferWhenAppsDoNotSupport() + || isTransferable + || hasRouteListingPreferenceItem) { + updateFullItemClickListener(v -> onItemClick(v, device)); + } setSingleLineLayout(device.getName(), false /* showSeekBar */, false /* showProgressBar */, true /* showCheckBox */, true /* showEndTouchArea */); diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java index 35c872f8a203..02a2befe44e5 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java @@ -941,6 +941,10 @@ public class MediaSwitchingController return mLocalMediaManager.getSelectableMediaDevice(); } + List<MediaDevice> getTransferableMediaDevices() { + return mLocalMediaManager.getTransferableMediaDevices(); + } + public List<MediaDevice> getSelectedMediaDevice() { if (!enableInputRouting()) { return mLocalMediaManager.getSelectedMediaDevice(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt index b5da044b886a..ab1326a8bafb 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt @@ -21,6 +21,7 @@ import android.provider.Settings import android.util.Log import com.android.systemui.animation.Expandable import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.qs.flags.QSComposeFragment import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler import com.android.systemui.qs.tiles.base.interactor.QSTileInput @@ -31,11 +32,14 @@ import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogEventLogger import javax.inject.Inject +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.withContext @SysUISingleton class ModesTileUserActionInteractor @Inject constructor( + @Main private val mainContext: CoroutineContext, private val qsTileIntentUserInputHandler: QSTileIntentUserInputHandler, // TODO(b/353896370): The domain layer should not have to depend on the UI layer. private val dialogDelegate: ModesDialogDelegate, @@ -65,7 +69,7 @@ constructor( dialogDelegate.showDialog(expandable) } - fun handleToggleClick(modesTileModel: ModesTileModel) { + suspend fun handleToggleClick(modesTileModel: ModesTileModel) { if (QSComposeFragment.isUnexpectedlyInLegacyMode()) { return } @@ -83,9 +87,11 @@ constructor( if (zenModeInteractor.shouldAskForZenDuration(dnd)) { dialogEventLogger.logOpenDurationDialog(dnd) - // NOTE: The dialog handles turning on the mode itself. - val dialog = dialogDelegate.makeDndDurationDialog() - dialog.show() + withContext(mainContext) { + // NOTE: The dialog handles turning on the mode itself. + val dialog = dialogDelegate.makeDndDurationDialog() + dialog.show() + } } else { dialogEventLogger.logModeOn(dnd) zenModeInteractor.activateMode(dnd) diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt index 40e6d284cbc9..ba7979ca2120 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt @@ -30,6 +30,7 @@ import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.classifier.Classifier import com.android.systemui.classifier.domain.interactor.FalsingInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.ui.viewmodel.LightRevealScrimViewModel import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator @@ -66,6 +67,7 @@ constructor( hapticsViewModelFactory: SceneContainerHapticsViewModel.Factory, val lightRevealScrim: LightRevealScrimViewModel, val wallpaperViewModel: WallpaperViewModel, + keyguardInteractor: KeyguardInteractor, @Assisted view: View, @Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit, ) : ExclusiveActivatable() { @@ -96,6 +98,14 @@ constructor( }, ) + /** Amount of color saturation for the Flexi🥃 ribbon. */ + val ribbonColorSaturation: Float by + hydrator.hydratedStateOf( + traceName = "ribbonColorSaturation", + source = keyguardInteractor.dozeAmount.map { 1 - it }, + initialValue = 1f, + ) + override suspend fun onActivated(): Nothing { try { // Sends a MotionEventHandler to the owner of the view-model so they can report diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt index cf310dd32613..5b44c082bd72 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt @@ -39,6 +39,7 @@ import com.android.systemui.recents.LauncherProxyService.LauncherProxyListener import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shared.system.QuickStepContract +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.policy.SplitShadeStateController import com.android.systemui.util.LargeScreenUtils @@ -155,8 +156,7 @@ constructor( val splitShadeEnabledChanged = newSplitShadeEnabled != splitShadeEnabled splitShadeEnabled = newSplitShadeEnabled largeScreenShadeHeaderActive = LargeScreenUtils.shouldUseLargeScreenShadeHeader(resources) - notificationsBottomMargin = - resources.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom) + notificationsBottomMargin = NotificationStackScrollLayout.getBottomMargin(context) largeScreenShadeHeaderHeight = calculateLargeShadeHeaderHeight() shadeHeaderHeight = calculateShadeHeaderHeight() panelMarginHorizontal = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt index 6ea72b97cb3a..521539866c9c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.view import android.content.Context import android.os.Bundle +import android.view.View import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.res.R import com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel.CastToOtherDeviceChipViewModel.Companion.CAST_TO_OTHER_DEVICE_ICON @@ -48,6 +49,11 @@ class EndCastScreenToOtherDeviceDialogDelegate( R.string.cast_to_other_device_stop_dialog_button, endMediaProjectionDialogHelper.wrapStopAction(stopAction), ) + if (com.android.media.projection.flags.Flags.showStopDialogPostCallEnd()) { + window + ?.decorView + ?.setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES) + } } } @@ -82,9 +88,7 @@ class EndCastScreenToOtherDeviceDialogDelegate( hostDeviceName, ) } else { - context.getString( - R.string.cast_to_other_device_stop_dialog_message_entire_screen, - ) + context.getString(R.string.cast_to_other_device_stop_dialog_message_entire_screen) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt index b0c832172776..8644c53f7849 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.view import android.content.Context import android.os.Bundle +import android.view.View import com.android.systemui.res.R import com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel.CastToOtherDeviceChipViewModel.Companion.CAST_TO_OTHER_DEVICE_ICON import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper @@ -59,6 +60,11 @@ class EndGenericCastToOtherDeviceDialogDelegate( R.string.cast_to_other_device_stop_dialog_button, endMediaProjectionDialogHelper.wrapStopAction(stopAction), ) + if (com.android.media.projection.flags.Flags.showStopDialogPostCallEnd()) { + window + ?.decorView + ?.setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/MediaProjectionStopDialogModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/MediaProjectionStopDialogModel.kt index b37c76232f01..52f55fca55bb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/MediaProjectionStopDialogModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/MediaProjectionStopDialogModel.kt @@ -34,6 +34,13 @@ sealed interface MediaProjectionStopDialogModel { */ fun createAndShowDialog() { val dialog = dialogDelegate.createDialog() + // Prevents the dialog from being dismissed by tapping outside its boundary. + // This is specifically required for the stop dialog shown at call end (i.e., + // PROJECTION_STARTED_DURING_CALL_AND_ACTIVE_POST_CALL event) to disallow remote + // dismissal by external devices. Other media projection stop dialogs do not require + // this since they are triggered explicitly by tapping the status bar chip, in which + // case the full screen containing the dialog is not remote dismissible. + dialog.setCanceledOnTouchOutside(/* cancel= */ false) dialog.setOnCancelListener { onDismissAction.invoke() } dialog.setOnDismissListener { onDismissAction.invoke() } dialog.show() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt index 72656ca1934c..4e0117ea3709 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.chips.screenrecord.ui.view import android.app.ActivityManager import android.content.Context import android.os.Bundle +import android.view.View import com.android.systemui.res.R import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper import com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel.ScreenRecordChipViewModel @@ -56,6 +57,11 @@ class EndScreenRecordingDialogDelegate( R.string.screenrecord_stop_dialog_button, endMediaProjectionDialogHelper.wrapStopAction(stopAction), ) + if (com.android.media.projection.flags.Flags.showStopDialogPostCallEnd()) { + window + ?.decorView + ?.setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegate.kt index 8ec05677107e..b8db6136e4c4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegate.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.chips.sharetoapp.ui.view import android.content.Context import android.os.Bundle +import android.view.View import com.android.systemui.res.R import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.ShareToAppChipViewModel.Companion.SHARE_TO_APP_ICON @@ -49,6 +50,11 @@ class EndGenericShareToAppDialogDelegate( R.string.share_to_app_stop_dialog_button, endMediaProjectionDialogHelper.wrapStopAction(stopAction), ) + if (com.android.media.projection.flags.Flags.showStopDialogPostCallEnd()) { + window + ?.decorView + ?.setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegate.kt index 053016e3109d..11a15555aef1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegate.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.chips.sharetoapp.ui.view import android.content.Context import android.os.Bundle +import android.view.View import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.res.R import com.android.systemui.statusbar.chips.mediaprojection.domain.model.ProjectionChipModel @@ -48,6 +49,11 @@ class EndShareScreenToAppDialogDelegate( R.string.share_to_app_stop_dialog_button, endMediaProjectionDialogHelper.wrapStopAction(stopAction), ) + if (com.android.media.projection.flags.Flags.showStopDialogPostCallEnd()) { + window + ?.decorView + ?.setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt index 375e02989a3d..cf2ec47a36d8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt @@ -16,12 +16,20 @@ package com.android.systemui.statusbar.chips.ui.compose -import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.CompositingStrategy +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.MeasureResult import androidx.compose.ui.layout.MeasureScope @@ -29,12 +37,15 @@ import androidx.compose.ui.node.LayoutModifierNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.constrain import androidx.compose.ui.unit.dp import com.android.systemui.res.R import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.chips.ui.viewmodel.rememberChronometerState +import kotlin.math.min @Composable fun ChipContent(viewModel: OngoingActivityChipModel.Shown, modifier: Modifier = Modifier) { @@ -43,6 +54,9 @@ fun ChipContent(viewModel: OngoingActivityChipModel.Shown, modifier: Modifier = val hasEmbeddedIcon = viewModel.icon is OngoingActivityChipModel.ChipIcon.StatusBarView || viewModel.icon is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon + val textStyle = MaterialTheme.typography.labelLarge + val textColor = Color(viewModel.colors.text(context)) + val maxTextWidth = dimensionResource(id = R.dimen.ongoing_activity_chip_max_text_width) val startPadding = if (isTextOnly || hasEmbeddedIcon) { 0.dp @@ -57,38 +71,69 @@ fun ChipContent(viewModel: OngoingActivityChipModel.Shown, modifier: Modifier = } else { 0.dp } - val textStyle = MaterialTheme.typography.labelLarge - val textColor = Color(viewModel.colors.text(context)) + val textMeasurer = rememberTextMeasurer() when (viewModel) { is OngoingActivityChipModel.Shown.Timer -> { val timerState = rememberChronometerState(startTimeMillis = viewModel.startTimeMs) + val text = timerState.currentTimeText Text( - text = timerState.currentTimeText, + text = text, style = textStyle, color = textColor, + softWrap = false, modifier = - modifier.padding(start = startPadding, end = endPadding).neverDecreaseWidth(), + modifier + .customTextContentLayout( + maxTextWidth = maxTextWidth, + startPadding = startPadding, + endPadding = endPadding, + ) { constraintWidth -> + val intrinsicWidth = + textMeasurer.measure(text, textStyle, softWrap = false).size.width + intrinsicWidth <= constraintWidth + } + .neverDecreaseWidth(), ) } is OngoingActivityChipModel.Shown.Countdown -> { - ChipText( - text = viewModel.secondsUntilStarted.toString(), + val text = viewModel.secondsUntilStarted.toString() + Text( + text = text, style = textStyle, color = textColor, - modifier = - modifier.padding(start = startPadding, end = endPadding).neverDecreaseWidth(), - backgroundColor = Color(viewModel.colors.background(context).defaultColor), + softWrap = false, + modifier = modifier.neverDecreaseWidth(), ) } is OngoingActivityChipModel.Shown.Text -> { - ChipText( - text = viewModel.text, - style = textStyle, + var hasOverflow by remember { mutableStateOf(false) } + val text = viewModel.text + Text( + text = text, color = textColor, - modifier = modifier.padding(start = startPadding, end = endPadding), - backgroundColor = Color(viewModel.colors.background(context).defaultColor), + style = textStyle, + softWrap = false, + modifier = + modifier + .customTextContentLayout( + maxTextWidth = maxTextWidth, + startPadding = startPadding, + endPadding = endPadding, + ) { constraintWidth -> + val intrinsicWidth = + textMeasurer.measure(text, textStyle, softWrap = false).size.width + hasOverflow = intrinsicWidth > constraintWidth + constraintWidth.toFloat() / intrinsicWidth.toFloat() > 0.5f + } + .overflowFadeOut( + hasOverflow = { hasOverflow }, + fadeLength = + dimensionResource( + id = R.dimen.ongoing_activity_chip_text_fading_edge_length + ), + ), ) } @@ -133,3 +178,83 @@ private class NeverDecreaseWidthNode : Modifier.Node(), LayoutModifierNode { return layout(width, height) { placeable.place(0, 0) } } } + +/** + * A custom layout modifier for text that ensures its text is only visible if a provided + * [shouldShow] callback returns true. Imposes a provided [maxTextWidthPx]. Also, accounts for + * provided padding values if provided and ensures its text is placed with the provided padding + * included around it. + */ +private fun Modifier.customTextContentLayout( + maxTextWidth: Dp, + startPadding: Dp = 0.dp, + endPadding: Dp = 0.dp, + shouldShow: (constraintWidth: Int) -> Boolean, +): Modifier { + return this.then( + CustomTextContentLayoutElement(maxTextWidth, startPadding, endPadding, shouldShow) + ) +} + +private data class CustomTextContentLayoutElement( + val maxTextWidth: Dp, + val startPadding: Dp, + val endPadding: Dp, + val shouldShow: (constrainedWidth: Int) -> Boolean, +) : ModifierNodeElement<CustomTextContentLayoutNode>() { + override fun create(): CustomTextContentLayoutNode { + return CustomTextContentLayoutNode(maxTextWidth, startPadding, endPadding, shouldShow) + } + + override fun update(node: CustomTextContentLayoutNode) { + node.shouldShow = shouldShow + node.maxTextWidth = maxTextWidth + node.startPadding = startPadding + node.endPadding = endPadding + } +} + +private class CustomTextContentLayoutNode( + var maxTextWidth: Dp, + var startPadding: Dp, + var endPadding: Dp, + var shouldShow: (constrainedWidth: Int) -> Boolean, +) : Modifier.Node(), LayoutModifierNode { + override fun MeasureScope.measure( + measurable: Measurable, + constraints: Constraints, + ): MeasureResult { + val horizontalPadding = startPadding + endPadding + val maxWidth = + min(maxTextWidth.roundToPx(), (constraints.maxWidth - horizontalPadding.roundToPx())) + .coerceAtLeast(constraints.minWidth) + val placeable = measurable.measure(constraints.copy(maxWidth = maxWidth)) + + val height = placeable.height + val width = placeable.width + return if (shouldShow(maxWidth)) { + layout(width + horizontalPadding.roundToPx(), height) { + placeable.place(startPadding.roundToPx(), 0) + } + } else { + layout(0, 0) {} + } + } +} + +private fun Modifier.overflowFadeOut(hasOverflow: () -> Boolean, fadeLength: Dp): Modifier { + return graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen).drawWithCache { + val width = size.width + val start = (width - fadeLength.toPx()).coerceAtLeast(0f) + val gradient = + Brush.horizontalGradient( + colors = listOf(Color.Black, Color.Transparent), + startX = start, + endX = width, + ) + onDrawWithContent { + drawContent() + if (hasOverflow()) drawRect(brush = gradient, blendMode = BlendMode.DstIn) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipText.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipText.kt deleted file mode 100644 index 3d768d2d3e1e..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipText.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.chips.ui.compose - -import androidx.compose.foundation.layout.sizeIn -import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.dimensionResource -import androidx.compose.ui.text.TextLayoutResult -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.rememberTextMeasurer -import com.android.systemui.res.R - -/** - * Renders text within a status bar chip. The text is only displayed if more than 50% of its width - * can fit inside the bounds of the chip. If there is any overflow, - * [R.dimen.ongoing_activity_chip_text_fading_edge_length] is used to fade out the edge of the text. - */ -@Composable -fun ChipText( - text: String, - backgroundColor: Color, - modifier: Modifier = Modifier, - color: Color = Color.Unspecified, - style: TextStyle = LocalTextStyle.current, - minimumVisibleRatio: Float = 0.5f, -) { - val density = LocalDensity.current - val textMeasurer = rememberTextMeasurer() - - val textFadeLength = - dimensionResource(id = R.dimen.ongoing_activity_chip_text_fading_edge_length) - val maxTextWidthDp = dimensionResource(id = R.dimen.ongoing_activity_chip_max_text_width) - val maxTextWidthPx = with(density) { maxTextWidthDp.toPx() } - - val textLayoutResult = remember(text, style) { textMeasurer.measure(text, style) } - val willOverflowWidth = textLayoutResult.size.width > maxTextWidthPx - - if (isSufficientlyVisible(maxTextWidthPx, minimumVisibleRatio, textLayoutResult)) { - Text( - text = text, - style = style, - softWrap = false, - color = color, - modifier = - modifier - .sizeIn(maxWidth = maxTextWidthDp) - .then( - if (willOverflowWidth) { - Modifier.overflowFadeOut( - with(density) { textFadeLength.roundToPx() }, - backgroundColor, - ) - } else { - Modifier - } - ), - ) - } -} - -private fun Modifier.overflowFadeOut(fadeLength: Int, color: Color): Modifier = drawWithContent { - drawContent() - - val brush = - Brush.horizontalGradient( - colors = listOf(Color.Transparent, color), - startX = size.width - fadeLength, - endX = size.width, - ) - drawRect( - brush = brush, - topLeft = Offset(size.width - fadeLength, 0f), - size = Size(fadeLength.toFloat(), size.height), - ) -} - -/** - * Returns `true` if at least [minimumVisibleRatio] of the text width fits within the given - * [maxAvailableWidthPx]. - */ -@Composable -private fun isSufficientlyVisible( - maxAvailableWidthPx: Float, - minimumVisibleRatio: Float, - textLayoutResult: TextLayoutResult, -): Boolean { - val widthPx = textLayoutResult.size.width - - return (maxAvailableWidthPx / widthPx) > minimumVisibleRatio -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt index 816f291b9273..b49d46c4a05b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.semantics.contentDescription @@ -42,6 +43,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import com.android.compose.animation.Expandable +import com.android.compose.modifiers.thenIf import com.android.systemui.animation.Expandable import com.android.systemui.common.ui.compose.Icon import com.android.systemui.common.ui.compose.load @@ -79,12 +81,11 @@ fun OngoingActivityChip(model: OngoingActivityChipModel.Shown, modifier: Modifie private fun ChipBody( model: OngoingActivityChipModel.Shown, modifier: Modifier = Modifier, - onClick: () -> Unit = {}, + onClick: (() -> Unit)? = null, ) { val context = LocalContext.current - val isClickable = onClick != {} + val isClickable = onClick != null val hasEmbeddedIcon = model.icon is OngoingActivityChipModel.ChipIcon.StatusBarView - val contentDescription = when (val icon = model.icon) { is OngoingActivityChipModel.ChipIcon.StatusBarView -> icon.contentDescription.load() @@ -93,17 +94,28 @@ private fun ChipBody( is OngoingActivityChipModel.ChipIcon.SingleColorIcon -> null null -> null } - + val chipSidePadding = dimensionResource(id = R.dimen.ongoing_activity_chip_side_padding) + val minWidth = + if (isClickable) { + dimensionResource(id = R.dimen.min_clickable_item_size) + } else if (model.icon != null) { + dimensionResource(id = R.dimen.ongoing_activity_chip_icon_size) + chipSidePadding + } else { + dimensionResource(id = R.dimen.ongoing_activity_chip_min_text_width) + chipSidePadding + } // Use a Box with `fillMaxHeight` to create a larger click surface for the chip. The visible // height of the chip is determined by the height of the background of the Row below. Box( contentAlignment = Alignment.Center, modifier = - modifier.fillMaxHeight().clickable(enabled = isClickable, onClick = onClick).semantics { - if (contentDescription != null) { - this.contentDescription = contentDescription - } - }, + modifier + .fillMaxHeight() + .clickable(enabled = isClickable, onClick = onClick ?: {}) + .semantics { + if (contentDescription != null) { + this.contentDescription = contentDescription + } + }, ) { Row( horizontalArrangement = Arrangement.Center, @@ -115,14 +127,15 @@ private fun ChipBody( ) ) .height(dimensionResource(R.dimen.ongoing_appops_chip_height)) - .widthIn( - min = - if (isClickable) { - dimensionResource(id = R.dimen.min_clickable_item_size) - } else { - 0.dp + .thenIf(isClickable) { Modifier.widthIn(min = minWidth) } + .layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + layout(placeable.width, placeable.height) { + if (constraints.maxWidth >= minWidth.roundToPx()) { + placeable.place(0, 0) } - ) + } + } .background(Color(model.colors.background(context).defaultColor)) .padding( horizontal = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 876090101f6e..ce1fc97cbffe 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -26,6 +26,7 @@ import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_ import static com.android.internal.jank.InteractionJankMonitor.CUJ_SHADE_CLEAR_ALL; import static com.android.systemui.Flags.magneticNotificationSwipes; import static com.android.systemui.Flags.notificationOverExpansionClippingFix; +import static com.android.systemui.Flags.notificationsRedesignFooterView; import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_SILENT; import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_SWIPE; import static com.android.systemui.statusbar.notification.stack.shared.model.AccessibilityScrollEvent.SCROLL_DOWN; @@ -694,11 +695,28 @@ public class NotificationStackScrollLayout protected void onFinishInflate() { super.onFinishInflate(); + if (notificationsRedesignFooterView()) { + int bottomMargin = getBottomMargin(getContext()); + MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams(); + lp.setMargins(lp.leftMargin, lp.topMargin, lp.rightMargin, bottomMargin); + setLayoutParams(lp); + } + if (!ModesEmptyShadeFix.isEnabled()) { inflateEmptyShadeView(); } } + /** Get the pixel value of the bottom margin, taking flags into account. */ + public static int getBottomMargin(Context context) { + Resources res = context.getResources(); + if (notificationsRedesignFooterView()) { + return res.getDimensionPixelSize(R.dimen.notification_2025_panel_margin_bottom); + } else { + return res.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom); + } + } + /** * Sets whether keyguard bypass is enabled. If true, this layout will be rendered in bypass * mode when it is on the keyguard. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index 06b989aaab57..673eb9f85ec3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -107,7 +107,7 @@ public class StackScrollAlgorithm { mGapHeightOnLockscreen = res.getDimensionPixelSize( R.dimen.notification_section_divider_height_lockscreen); mNotificationScrimPadding = res.getDimensionPixelSize(R.dimen.notification_side_paddings); - mMarginBottom = res.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom); + mMarginBottom = NotificationStackScrollLayout.getBottomMargin(context); mQuickQsOffsetHeight = SystemBarUtils.getQuickQsOffsetHeight(context); mSmallCornerRadius = res.getDimension(R.dimen.notification_corner_radius_small); mLargeCornerRadius = res.getDimension(R.dimen.notification_corner_radius); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt index 107875df6bfa..c8b3341a0240 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt @@ -26,6 +26,7 @@ import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.LargeScreenHeaderHelper import com.android.systemui.shade.ShadeDisplayAware +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout import com.android.systemui.statusbar.policy.SplitShadeStateController import dagger.Lazy import javax.inject.Inject @@ -79,8 +80,7 @@ constructor( getBoolean(R.bool.config_use_large_screen_shade_header), marginHorizontal = getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal), - marginBottom = - getDimensionPixelSize(R.dimen.notification_panel_margin_bottom), + marginBottom = NotificationStackScrollLayout.getBottomMargin(context), marginTop = getDimensionPixelSize(R.dimen.notification_panel_margin_top), marginTopLargeScreen = largeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight(), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index b6d89ecf29f2..d6e4add4eee3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -80,6 +80,7 @@ import com.android.systemui.shade.shared.model.ShadeMode.Dual import com.android.systemui.shade.shared.model.ShadeMode.Single import com.android.systemui.shade.shared.model.ShadeMode.Split import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor @@ -263,8 +264,7 @@ constructor( horizontalPosition = horizontalPosition, marginStart = if (shadeMode is Split) 0 else marginHorizontal, marginEnd = marginHorizontal, - marginBottom = - getDimensionPixelSize(R.dimen.notification_panel_margin_bottom), + marginBottom = NotificationStackScrollLayout.getBottomMargin(context), // y position of the NSSL in the window needs to be 0 under scene // container marginTop = 0, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt index 5a63c0cd84e6..bd1d7f755a74 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt @@ -30,6 +30,7 @@ import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.util.concurrency.DelayableExecutor import dagger.Lazy import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope /** Handles start activity logic in SystemUI. */ @SysUISingleton @@ -52,9 +53,10 @@ constructor( override fun registerTransition( cookie: ActivityTransitionAnimator.TransitionCookie, controllerFactory: ActivityTransitionAnimator.ControllerFactory, + scope: CoroutineScope, ) { if (!TransitionAnimator.longLivedReturnAnimationsEnabled()) return - activityStarterInternal.registerTransition(cookie, controllerFactory) + activityStarterInternal.registerTransition(cookie, controllerFactory, scope) } override fun unregisterTransition(cookie: ActivityTransitionAnimator.TransitionCookie) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternal.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternal.kt index 5e427fbf1f7e..015ec3052134 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternal.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternal.kt @@ -25,15 +25,17 @@ import android.view.View import com.android.systemui.ActivityIntentHelper import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.plugins.ActivityStarter +import kotlinx.coroutines.CoroutineScope interface ActivityStarterInternal { /** * Registers the given [controllerFactory] for launching and closing transitions matching the - * [cookie] and the [ComponentName] that it contains. + * [cookie] and the [ComponentName] that it contains, within the given [scope]. */ fun registerTransition( cookie: ActivityTransitionAnimator.TransitionCookie, controllerFactory: ActivityTransitionAnimator.ControllerFactory, + scope: CoroutineScope, ) /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt index 7289c2ed5897..6e82d7f7401a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt @@ -66,6 +66,7 @@ import com.android.systemui.util.kotlin.getOrNull import dagger.Lazy import java.util.Optional import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope /** * Encapsulates the activity logic for activity starter when the SceneContainerFlag is enabled. @@ -105,6 +106,7 @@ constructor( override fun registerTransition( cookie: ActivityTransitionAnimator.TransitionCookie, controllerFactory: ActivityTransitionAnimator.ControllerFactory, + scope: CoroutineScope, ) { check(TransitionAnimator.longLivedReturnAnimationsEnabled()) @@ -116,7 +118,7 @@ constructor( controllerFactory.launchCujType, controllerFactory.returnCujType, ) { - override fun createController( + override suspend fun createController( forLaunch: Boolean ): ActivityTransitionAnimator.Controller { val baseController = controllerFactory.createController(forLaunch) @@ -132,7 +134,7 @@ constructor( } } - activityTransitionAnimator.register(cookie, factory) + activityTransitionAnimator.register(cookie, factory, scope) } override fun unregisterTransition(cookie: ActivityTransitionAnimator.TransitionCookie) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt index d7a29c36f2ce..76f67dc6c146 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt @@ -64,6 +64,7 @@ import com.android.systemui.util.kotlin.getOrNull import dagger.Lazy import java.util.Optional import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope /** Encapsulates the activity logic for activity starter. */ @SysUISingleton @@ -102,6 +103,7 @@ constructor( override fun registerTransition( cookie: ActivityTransitionAnimator.TransitionCookie, controllerFactory: ActivityTransitionAnimator.ControllerFactory, + scope: CoroutineScope, ) { check(TransitionAnimator.longLivedReturnAnimationsEnabled()) @@ -113,7 +115,7 @@ constructor( controllerFactory.launchCujType, controllerFactory.returnCujType, ) { - override fun createController( + override suspend fun createController( forLaunch: Boolean ): ActivityTransitionAnimator.Controller { val baseController = controllerFactory.createController(forLaunch) @@ -129,7 +131,7 @@ constructor( } } - activityTransitionAnimator.register(cookie, factory) + activityTransitionAnimator.register(cookie, factory, scope) } override fun unregisterTransition(cookie: ActivityTransitionAnimator.TransitionCookie) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModel.kt index 6258a55c374f..34ba767c227e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModel.kt @@ -22,7 +22,7 @@ import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.qs.tileimpl.QSTileImpl import com.android.systemui.qs.tileimpl.QSTileImpl.ResourceIcon import com.android.systemui.res.R @@ -61,7 +61,7 @@ constructor( mobileIconsInteractor: MobileIconsInteractor, wifiInteractor: WifiInteractor, private val context: Context, - @Application scope: CoroutineScope, + @Background scope: CoroutineScope, ) { private val internetLabel: String = context.getString(R.string.quick_settings_internet_label) @@ -111,17 +111,16 @@ constructor( if (it == null) { notConnectedFlow } else { - combine( - it.networkName, - it.signalLevelIcon, - mobileDataContentName, - ) { networkNameModel, signalIcon, dataContentDescription -> + combine(it.networkName, it.signalLevelIcon, mobileDataContentName) { + networkNameModel, + signalIcon, + dataContentDescription -> when (signalIcon) { is SignalIconModel.Cellular -> { val secondary = mobileDataContentConcat( networkNameModel.name, - dataContentDescription + dataContentDescription, ) InternetTileModel.Active( secondaryTitle = secondary, @@ -147,7 +146,7 @@ constructor( private fun mobileDataContentConcat( networkName: String?, - dataContentDescription: CharSequence? + dataContentDescription: CharSequence?, ): CharSequence { if (dataContentDescription == null) { return networkName ?: "" @@ -160,9 +159,9 @@ constructor( context.getString( R.string.mobile_carrier_text_format, networkName, - dataContentDescription + dataContentDescription, ), - 0 + 0, ) } @@ -191,10 +190,9 @@ constructor( } private val notConnectedFlow: StateFlow<InternetTileModel> = - combine( - wifiInteractor.areNetworksAvailable, - airplaneModeRepository.isAirplaneMode, - ) { networksAvailable, isAirplaneMode -> + combine(wifiInteractor.areNetworksAvailable, airplaneModeRepository.isAirplaneMode) { + networksAvailable, + isAirplaneMode -> when { isAirplaneMode -> { val secondary = context.getString(R.string.status_bar_airplane) @@ -213,7 +211,7 @@ constructor( iconId = R.drawable.ic_qs_no_internet_available, stateDescription = null, contentDescription = - ContentDescription.Loaded("$internetLabel,$secondary") + ContentDescription.Loaded("$internetLabel,$secondary"), ) } else -> { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt index db5f1301823b..2fc22867e702 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.policy.ui.dialog +import android.annotation.UiThread; import android.app.Dialog import android.content.Context import android.content.Intent @@ -205,6 +206,7 @@ constructor( * Special dialog to ask the user for the duration of DND. Not to be confused with the modes * dialog itself. */ + @UiThread fun makeDndDurationDialog(): Dialog { val dialog = EnableDndDialogFactory( diff --git a/packages/SystemUI/tests/goldens/brightnessSlider_iconAlphaChanges.json b/packages/SystemUI/tests/goldens/brightnessSlider_iconAlphaChanges.json new file mode 100644 index 000000000000..cefada71686c --- /dev/null +++ b/packages/SystemUI/tests/goldens/brightnessSlider_iconAlphaChanges.json @@ -0,0 +1,168 @@ +{ + "frame_ids": [ + "before", + 0, + 16, + 32, + 48, + 64, + 80, + 96, + 112, + 128, + 144, + 160, + 176, + 192, + 208, + 224, + 240, + 256, + 272, + 288, + 304, + 320, + 336, + 352, + 368, + 384, + 400, + 416, + 432, + 448, + 464, + 480, + 496, + 512, + 528, + 544, + 560, + 576, + 592, + 608, + 624, + 640, + 656, + 672, + 688, + 704, + 720, + 736, + 752, + "after" + ], + "features": [ + { + "name": "activeIconAlpha_activeIconAlpha", + "type": "float", + "data_points": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0.5782508, + 0.09543866, + 8.595586E-4, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "name": "inactiveIconAlpha_inactiveIconAlpha", + "type": "float", + "data_points": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.065971695, + 0.3946195, + 0.7348632, + 0.8979182, + 0.97246534, + 0.9986459, + 1 + ] + } + ] +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index 312d2ffd74e4..4110a05170b3 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -18,7 +18,6 @@ package com.android.keyguard; import static android.app.StatusBarManager.SESSION_KEYGUARD; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; -import static android.hardware.biometrics.BiometricAuthenticator.TYPE_ANY_BIOMETRIC; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT; @@ -2717,7 +2716,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { () -> mAlternateBouncerInteractor, () -> mJavaAdapter, () -> mSceneInteractor, - mCommunalSceneInteractor); + () -> mCommunalSceneInteractor); setAlternateBouncerVisibility(false); setPrimaryBouncerVisibility(false); setStrongAuthTracker(KeyguardUpdateMonitorTest.this.mStrongAuthTracker); 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 ad5f96044c4c..a0f5b2214f80 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java @@ -242,8 +242,8 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { mWindowMagnificationAnimationController.enableWindowMagnification(targetScale, targetCenterX, targetCenterY, mAnimationCallback2); mCurrentScale.set(mController.getScale()); - mCurrentCenterX.set(mController.getCenterX()); - mCurrentCenterY.set(mController.getCenterY()); + mCurrentCenterX.set(mController.getMagnificationFrameCenterX()); + mCurrentCenterY.set(mController.getMagnificationFrameCenterY()); advanceTimeBy(mWaitAnimationDuration); }); @@ -297,8 +297,8 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { mWindowMagnificationAnimationController.enableWindowMagnification(targetScale, targetCenterX, targetCenterY, mAnimationCallback); mCurrentScale.set(mController.getScale()); - mCurrentCenterX.set(mController.getCenterX()); - mCurrentCenterY.set(mController.getCenterY()); + mCurrentCenterX.set(mController.getMagnificationFrameCenterX()); + mCurrentCenterY.set(mController.getMagnificationFrameCenterY()); advanceTimeBy(mWaitAnimationDuration); }); @@ -339,8 +339,8 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { mWindowMagnificationAnimationController.enableWindowMagnification(targetScale, targetCenterX, targetCenterY, mAnimationCallback); mCurrentScale.set(mController.getScale()); - mCurrentCenterX.set(mController.getCenterX()); - mCurrentCenterY.set(mController.getCenterY()); + mCurrentCenterX.set(mController.getMagnificationFrameCenterX()); + mCurrentCenterY.set(mController.getMagnificationFrameCenterY()); advanceTimeBy(mWaitAnimationDuration); }); @@ -375,8 +375,8 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { mWindowMagnificationAnimationController.enableWindowMagnification(targetScale, targetCenterX, targetCenterY, mAnimationCallback); mCurrentScale.set(mController.getScale()); - mCurrentCenterX.set(mController.getCenterX()); - mCurrentCenterY.set(mController.getCenterY()); + mCurrentCenterX.set(mController.getMagnificationFrameCenterX()); + mCurrentCenterY.set(mController.getMagnificationFrameCenterY()); advanceTimeBy(mWaitAnimationDuration); }); @@ -463,8 +463,8 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { mWindowMagnificationAnimationController.enableWindowMagnification(targetScale, targetCenterX, targetCenterY, mAnimationCallback2); mCurrentScale.set(mController.getScale()); - mCurrentCenterX.set(mController.getCenterX()); - mCurrentCenterY.set(mController.getCenterY()); + mCurrentCenterX.set(mController.getMagnificationFrameCenterX()); + mCurrentCenterY.set(mController.getMagnificationFrameCenterY()); }); // Current spec shouldn't match given spec. @@ -548,8 +548,8 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { mWindowMagnificationAnimationController.enableWindowMagnification(targetScale, targetCenterX, targetCenterY, mAnimationCallback2); mCurrentScale.set(mController.getScale()); - mCurrentCenterX.set(mController.getCenterX()); - mCurrentCenterY.set(mController.getCenterY()); + mCurrentCenterX.set(mController.getMagnificationFrameCenterX()); + mCurrentCenterY.set(mController.getMagnificationFrameCenterY()); advanceTimeBy(mWaitAnimationDuration); }); @@ -777,8 +777,8 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { mWindowMagnificationAnimationController.deleteWindowMagnification( mAnimationCallback2); mCurrentScale.set(mController.getScale()); - mCurrentCenterX.set(mController.getCenterX()); - mCurrentCenterY.set(mController.getCenterY()); + mCurrentCenterX.set(mController.getMagnificationFrameCenterX()); + mCurrentCenterY.set(mController.getMagnificationFrameCenterY()); // ValueAnimator.reverse() could not work correctly with the AnimatorTestRule since it // is using SystemClock in reverse() (b/305731398). Therefore, we call end() on the // animator directly to verify the result of animation is correct instead of querying @@ -940,8 +940,8 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { private void verifyFinalSpec(float expectedScale, float expectedCenterX, float expectedCenterY) { assertEquals(expectedScale, mController.getScale(), 0f); - assertEquals(expectedCenterX, mController.getCenterX(), 0f); - assertEquals(expectedCenterY, mController.getCenterY(), 0f); + assertEquals(expectedCenterX, mController.getMagnificationFrameCenterX(), 0f); + assertEquals(expectedCenterY, mController.getMagnificationFrameCenterY(), 0f); } private void enableWindowMagnificationWithoutAnimation() { 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 8552e48a2024..7cf93277bb5b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java @@ -63,6 +63,8 @@ import android.graphics.Rect; import android.os.Handler; import android.os.RemoteException; import android.os.SystemClock; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.provider.Settings; import android.testing.TestableLooper; import android.testing.TestableResources; @@ -88,6 +90,7 @@ import androidx.test.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; 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; @@ -128,6 +131,7 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(/* test= */ null); private static final int LAYOUT_CHANGE_TIMEOUT_MS = 5000; + private static final int INSET_BOTTOM = 10; @Mock private MirrorWindowControl mMirrorWindowControl; @Mock @@ -329,9 +333,9 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { final ArgumentCaptor<Rect> sourceBoundsCaptor = ArgumentCaptor.forClass(Rect.class); verify(mWindowMagnifierCallback, atLeast(2)).onSourceBoundsChanged( (eq(mContext.getDisplayId())), sourceBoundsCaptor.capture()); - assertThat(mWindowMagnificationController.getCenterX()) + assertThat(mWindowMagnificationController.getMagnificationFrameCenterX()) .isEqualTo(sourceBoundsCaptor.getValue().exactCenterX()); - assertThat(mWindowMagnificationController.getCenterY()) + assertThat(mWindowMagnificationController.getMagnificationFrameCenterY()) .isEqualTo(sourceBoundsCaptor.getValue().exactCenterY()); } @@ -382,6 +386,7 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { } @Test + @DisableFlags(Flags.FLAG_UPDATE_WINDOW_MAGNIFIER_BOTTOM_BOUNDARY) public void deleteWindowMagnification_enableAtTheBottom_overlapFlagIsFalse() { final WindowManager wm = mContext.getSystemService(WindowManager.class); final Rect bounds = wm.getCurrentWindowMetrics().getBounds(); @@ -457,12 +462,14 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { verify(mAnimationCallback, never()).onResult(eq(false)); verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS)) .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture()); - assertThat(mWindowMagnificationController.getCenterX()) + assertThat(mWindowMagnificationController.getMagnificationFrameCenterX()) .isEqualTo(sourceBoundsCaptor.getValue().exactCenterX()); - assertThat(mWindowMagnificationController.getCenterY()) + assertThat(mWindowMagnificationController.getMagnificationFrameCenterY()) .isEqualTo(sourceBoundsCaptor.getValue().exactCenterY()); - assertThat(mWindowMagnificationController.getCenterX()).isEqualTo(targetCenterX); - assertThat(mWindowMagnificationController.getCenterY()).isEqualTo(targetCenterY); + assertThat(mWindowMagnificationController.getMagnificationFrameCenterX()) + .isEqualTo(targetCenterX); + assertThat(mWindowMagnificationController.getMagnificationFrameCenterY()) + .isEqualTo(targetCenterY); } @Test @@ -498,12 +505,14 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { verify(mAnimationCallback, times(3)).onResult(eq(false)); verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS)) .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture()); - assertThat(mWindowMagnificationController.getCenterX()) + assertThat(mWindowMagnificationController.getMagnificationFrameCenterX()) .isEqualTo(sourceBoundsCaptor.getValue().exactCenterX()); - assertThat(mWindowMagnificationController.getCenterY()) + assertThat(mWindowMagnificationController.getMagnificationFrameCenterY()) .isEqualTo(sourceBoundsCaptor.getValue().exactCenterY()); - assertThat(mWindowMagnificationController.getCenterX()).isEqualTo(centerX + 40); - assertThat(mWindowMagnificationController.getCenterY()).isEqualTo(centerY + 40); + assertThat(mWindowMagnificationController.getMagnificationFrameCenterX()) + .isEqualTo(centerX + 40); + assertThat(mWindowMagnificationController.getMagnificationFrameCenterY()) + .isEqualTo(centerY + 40); } @Test @@ -545,8 +554,8 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { mWindowMagnificationController.updateWindowMagnificationInternal(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()); + magnifiedCenter.set(mWindowMagnificationController.getMagnificationFrameCenterX(), + mWindowMagnificationController.getMagnificationFrameCenterY()); }); // Rotate the window clockwise 90 degree. windowBounds.set(windowBounds.top, windowBounds.left, windowBounds.bottom, @@ -559,8 +568,9 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { assertThat(mWindowMagnificationController.mRotation).isEqualTo(newRotation); final PointF expectedCenter = new PointF(magnifiedCenter.y, displayWidth - magnifiedCenter.x); - final PointF actualCenter = new PointF(mWindowMagnificationController.getCenterX(), - mWindowMagnificationController.getCenterY()); + final PointF actualCenter = + new PointF(mWindowMagnificationController.getMagnificationFrameCenterX(), + mWindowMagnificationController.getMagnificationFrameCenterY()); assertThat(actualCenter).isEqualTo(expectedCenter); } @@ -603,10 +613,10 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { }); // The ratio of center to window size should be the same. - assertThat(mWindowMagnificationController.getCenterX() / testWindowBounds.width()) - .isEqualTo(expectedRatio); - assertThat(mWindowMagnificationController.getCenterY() / testWindowBounds.height()) - .isEqualTo(expectedRatio); + assertThat(mWindowMagnificationController.getMagnificationFrameCenterX() + / testWindowBounds.width()).isEqualTo(expectedRatio); + assertThat(mWindowMagnificationController.getMagnificationFrameCenterY() + / testWindowBounds.height()).isEqualTo(expectedRatio); } @Test @@ -1175,6 +1185,7 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { } @Test + @DisableFlags(Flags.FLAG_UPDATE_WINDOW_MAGNIFIER_BOTTOM_BOUNDARY) public void moveWindowMagnificationToTheBottom_enabledWithGestureInset_overlapFlagIsTrue() { final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds(); setSystemGestureInsets(); @@ -1191,6 +1202,30 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { } @Test + @EnableFlags(Flags.FLAG_UPDATE_WINDOW_MAGNIFIER_BOTTOM_BOUNDARY) + public void moveWindowMagnificationToTheBottom_stopsAtSystemGestureTop() { + final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + setSystemGestureInsets(); + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.updateWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + }); + + ViewGroup.LayoutParams params = mSurfaceControlViewHost.getView().getLayoutParams(); + final int mOuterBorderSize = mResources.getDimensionPixelSize( + R.dimen.magnification_outer_border_margin); + + final float expectedY = + (float) (bounds.bottom - INSET_BOTTOM - params.height + mOuterBorderSize); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.moveWindowMagnifier(0, bounds.height()); + }); + + assertThat(mWindowMagnificationController.getMagnifierWindowY()).isEqualTo(expectedY); + } + + @Test public void moveWindowMagnificationToRightEdge_dragHandleMovesToLeftAndUpdatesTapExcludeRegion() throws RemoteException { final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds(); @@ -1445,8 +1480,10 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { mInstrumentation.runOnMainSync(() -> { mWindowMagnificationController.setWindowSizeAndCenter(minimumWindowSize, minimumWindowSize, bounds.right, bounds.bottom); - magnificationCenterX.set((int) mWindowMagnificationController.getCenterX()); - magnificationCenterY.set((int) mWindowMagnificationController.getCenterY()); + magnificationCenterX.set( + (int) mWindowMagnificationController.getMagnificationFrameCenterX()); + magnificationCenterY.set( + (int) mWindowMagnificationController.getMagnificationFrameCenterY()); }); assertThat(magnificationCenterX.get()).isLessThan(bounds.right); @@ -1501,7 +1538,7 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { private void setSystemGestureInsets() { final WindowInsets testInsets = new WindowInsets.Builder() - .setInsets(systemGestures(), Insets.of(0, 0, 0, 10)) + .setInsets(systemGestures(), Insets.of(0, 0, 0, INSET_BOTTOM)) .build(); mWindowManager.setWindowInsets(testInsets); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt index fd751d9cc7c3..845be0252581 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt @@ -27,7 +27,10 @@ import android.window.WindowAnimationState import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope import com.android.systemui.shared.Flags +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.wm.shell.shared.ShellTransitions import com.google.common.truth.Truth.assertThat @@ -38,6 +41,9 @@ import junit.framework.Assert.assertTrue import junit.framework.AssertionFailedError import kotlin.concurrent.thread import kotlin.test.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Assert.assertThrows import org.junit.Before @@ -54,10 +60,12 @@ import org.mockito.Mockito.`when` import org.mockito.Spy import org.mockito.junit.MockitoJUnit +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) @RunWithLooper class ActivityTransitionAnimatorTest : SysuiTestCase() { + private val kosmos = testKosmos() private val transitionContainer = LinearLayout(mContext) private val mainExecutor = context.mainExecutor private val testTransitionAnimator = fakeTransitionAnimator(mainExecutor) @@ -67,12 +75,12 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { @Spy private val controller = TestTransitionAnimatorController(transitionContainer) @Mock lateinit var iCallback: IRemoteAnimationFinishedCallback - private lateinit var activityTransitionAnimator: ActivityTransitionAnimator + private lateinit var underTest: ActivityTransitionAnimator @get:Rule val rule = MockitoJUnit.rule() @Before fun setup() { - activityTransitionAnimator = + underTest = ActivityTransitionAnimator( mainExecutor, ActivityTransitionAnimator.TransitionRegister.fromShellTransitions( @@ -82,17 +90,17 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { testTransitionAnimator, disableWmTimeout = true, ) - activityTransitionAnimator.callback = callback - activityTransitionAnimator.addListener(listener) + underTest.callback = callback + underTest.addListener(listener) } @After fun tearDown() { - activityTransitionAnimator.removeListener(listener) + underTest.removeListener(listener) } private fun startIntentWithAnimation( - animator: ActivityTransitionAnimator = this.activityTransitionAnimator, + animator: ActivityTransitionAnimator = underTest, controller: ActivityTransitionAnimator.Controller? = this.controller, animate: Boolean = true, intentStarter: (RemoteAnimationAdapter?) -> Int, @@ -157,7 +165,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java) var animationAdapter: RemoteAnimationAdapter? = null - startIntentWithAnimation(activityTransitionAnimator) { adapter -> + startIntentWithAnimation(underTest) { adapter -> animationAdapter = adapter ActivityManager.START_DELIVERED_TO_TOP } @@ -185,9 +193,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { fun registersReturnIffCookieIsPresent() { `when`(callback.isOnKeyguard()).thenReturn(false) - startIntentWithAnimation(activityTransitionAnimator, controller) { _ -> - ActivityManager.START_DELIVERED_TO_TOP - } + startIntentWithAnimation(underTest, controller) { ActivityManager.START_DELIVERED_TO_TOP } waitForIdleSync() assertTrue(testShellTransitions.remotes.isEmpty()) @@ -199,9 +205,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { get() = ActivityTransitionAnimator.TransitionCookie("testCookie") } - startIntentWithAnimation(activityTransitionAnimator, controller) { _ -> - ActivityManager.START_DELIVERED_TO_TOP - } + startIntentWithAnimation(underTest, controller) { ActivityManager.START_DELIVERED_TO_TOP } waitForIdleSync() assertEquals(1, testShellTransitions.remotes.size) @@ -214,13 +218,15 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { ) @Test fun registersLongLivedTransition() { - var factory = controllerFactory() - activityTransitionAnimator.register(factory.cookie, factory) - assertEquals(2, testShellTransitions.remotes.size) - - factory = controllerFactory() - activityTransitionAnimator.register(factory.cookie, factory) - assertEquals(4, testShellTransitions.remotes.size) + kosmos.runTest { + var factory = controllerFactory() + underTest.register(factory.cookie, factory, testScope) + assertEquals(2, testShellTransitions.remotes.size) + + factory = controllerFactory() + underTest.register(factory.cookie, factory, testScope) + assertEquals(4, testShellTransitions.remotes.size) + } } @EnableFlags( @@ -229,49 +235,55 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { ) @Test fun registersLongLivedTransitionOverridingPreviousRegistration() { - val cookie = ActivityTransitionAnimator.TransitionCookie("test_cookie") - var factory = controllerFactory(cookie) - activityTransitionAnimator.register(cookie, factory) - val transitions = testShellTransitions.remotes.values.toList() - - factory = controllerFactory(cookie) - activityTransitionAnimator.register(cookie, factory) - assertEquals(2, testShellTransitions.remotes.size) - for (transition in transitions) { - assertThat(testShellTransitions.remotes.values).doesNotContain(transition) + kosmos.runTest { + val cookie = ActivityTransitionAnimator.TransitionCookie("test_cookie") + var factory = controllerFactory(cookie) + underTest.register(cookie, factory, testScope) + val transitions = testShellTransitions.remotes.values.toList() + + factory = controllerFactory(cookie) + underTest.register(cookie, factory, testScope) + assertEquals(2, testShellTransitions.remotes.size) + for (transition in transitions) { + assertThat(testShellTransitions.remotes.values).doesNotContain(transition) + } } } @DisableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED) @Test fun doesNotRegisterLongLivedTransitionIfFlagIsDisabled() { - val factory = controllerFactory(component = null) - assertThrows(IllegalStateException::class.java) { - activityTransitionAnimator.register(factory.cookie, factory) + kosmos.runTest { + val factory = controllerFactory(component = null) + assertThrows(IllegalStateException::class.java) { + underTest.register(factory.cookie, factory, testScope) + } } } @EnableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED) @Test fun doesNotRegisterLongLivedTransitionIfMissingRequiredProperties() { - // No ComponentName - var factory = controllerFactory(component = null) - assertThrows(IllegalStateException::class.java) { - activityTransitionAnimator.register(factory.cookie, factory) - } + kosmos.runTest { + // No ComponentName + var factory = controllerFactory(component = null) + assertThrows(IllegalStateException::class.java) { + underTest.register(factory.cookie, factory, testScope) + } - // No TransitionRegister - activityTransitionAnimator = - ActivityTransitionAnimator( - mainExecutor, - transitionRegister = null, - testTransitionAnimator, - testTransitionAnimator, - disableWmTimeout = true, - ) - factory = controllerFactory() - assertThrows(IllegalStateException::class.java) { - activityTransitionAnimator.register(factory.cookie, factory) + // No TransitionRegister + val activityTransitionAnimator = + ActivityTransitionAnimator( + mainExecutor, + transitionRegister = null, + testTransitionAnimator, + testTransitionAnimator, + disableWmTimeout = true, + ) + factory = controllerFactory() + assertThrows(IllegalStateException::class.java) { + activityTransitionAnimator.register(factory.cookie, factory, testScope) + } } } @@ -281,27 +293,29 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { ) @Test fun unregistersLongLivedTransition() { - val cookies = arrayOfNulls<ActivityTransitionAnimator.TransitionCookie>(3) + kosmos.runTest { + val cookies = arrayOfNulls<ActivityTransitionAnimator.TransitionCookie>(3) - for (index in 0 until 3) { - cookies[index] = mock(ActivityTransitionAnimator.TransitionCookie::class.java) - val factory = controllerFactory(cookies[index]!!) - activityTransitionAnimator.register(factory.cookie, factory) - } + for (index in 0 until 3) { + cookies[index] = mock(ActivityTransitionAnimator.TransitionCookie::class.java) + val factory = controllerFactory(cookies[index]!!) + underTest.register(factory.cookie, factory, testScope) + } - activityTransitionAnimator.unregister(cookies[0]!!) - assertEquals(4, testShellTransitions.remotes.size) + underTest.unregister(cookies[0]!!) + assertEquals(4, testShellTransitions.remotes.size) - activityTransitionAnimator.unregister(cookies[2]!!) - assertEquals(2, testShellTransitions.remotes.size) + underTest.unregister(cookies[2]!!) + assertEquals(2, testShellTransitions.remotes.size) - activityTransitionAnimator.unregister(cookies[1]!!) - assertThat(testShellTransitions.remotes).isEmpty() + underTest.unregister(cookies[1]!!) + assertThat(testShellTransitions.remotes).isEmpty() + } } @Test fun doesNotStartIfAnimationIsCancelled() { - val runner = activityTransitionAnimator.createEphemeralRunner(controller) + val runner = underTest.createEphemeralRunner(controller) runner.onAnimationCancelled() runner.onAnimationStart(TRANSIT_NONE, emptyArray(), emptyArray(), emptyArray(), iCallback) @@ -315,7 +329,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { @Test fun cancelsIfNoOpeningWindowIsFound() { - val runner = activityTransitionAnimator.createEphemeralRunner(controller) + val runner = underTest.createEphemeralRunner(controller) runner.onAnimationStart(TRANSIT_NONE, emptyArray(), emptyArray(), emptyArray(), iCallback) waitForIdleSync() @@ -328,7 +342,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { @Test fun startsAnimationIfWindowIsOpening() { - val runner = activityTransitionAnimator.createEphemeralRunner(controller) + val runner = underTest.createEphemeralRunner(controller) runner.onAnimationStart( TRANSIT_NONE, arrayOf(fakeWindow()), @@ -354,9 +368,11 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { ) @Test fun creatingRunnerWithLazyInitializationThrows_whenTheFlagsAreDisabled() { - assertThrows(IllegalStateException::class.java) { - val factory = controllerFactory() - activityTransitionAnimator.createLongLivedRunner(factory, forLaunch = true) + kosmos.runTest { + assertThrows(IllegalStateException::class.java) { + val factory = controllerFactory() + underTest.createLongLivedRunner(factory, testScope, forLaunch = true) + } } } @@ -365,44 +381,34 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, ) @Test - fun runnerCreatesDelegateLazily_whenPostingTimeouts() { - val factory = controllerFactory() - val runner = activityTransitionAnimator.createLongLivedRunner(factory, forLaunch = true) - assertNull(runner.delegate) - runner.postTimeouts() - assertNotNull(runner.delegate) - } - - @EnableFlags( - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, - ) - @Test fun runnerCreatesDelegateLazily_onAnimationStart() { - val factory = controllerFactory() - val runner = activityTransitionAnimator.createLongLivedRunner(factory, forLaunch = true) - assertNull(runner.delegate) - - var delegateInitialized = false - activityTransitionAnimator.addListener( - object : ActivityTransitionAnimator.Listener { - override fun onTransitionAnimationStart() { - // This is called iff the delegate was initialized, so it's a good proxy for - // checking the initialization. - delegateInitialized = true + kosmos.runTest { + val factory = controllerFactory() + val runner = underTest.createLongLivedRunner(factory, testScope, forLaunch = true) + assertNull(runner.delegate) + + var delegateInitialized = false + underTest.addListener( + object : ActivityTransitionAnimator.Listener { + override fun onTransitionAnimationStart() { + // This is called iff the delegate was initialized, so it's a good proxy for + // checking the initialization. + delegateInitialized = true + } } - } - ) - runner.onAnimationStart( - TRANSIT_NONE, - arrayOf(fakeWindow()), - emptyArray(), - emptyArray(), - iCallback, - ) + ) + runner.onAnimationStart( + TRANSIT_NONE, + arrayOf(fakeWindow()), + emptyArray(), + emptyArray(), + iCallback, + ) + testScope.advanceUntilIdle() + waitForIdleSync() - waitForIdleSync() - assertTrue(delegateInitialized) + assertTrue(delegateInitialized) + } } @EnableFlags( @@ -411,29 +417,32 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { ) @Test fun runnerCreatesDelegateLazily_onAnimationTakeover() { - val factory = controllerFactory() - val runner = activityTransitionAnimator.createLongLivedRunner(factory, forLaunch = false) - assertNull(runner.delegate) - - var delegateInitialized = false - activityTransitionAnimator.addListener( - object : ActivityTransitionAnimator.Listener { - override fun onTransitionAnimationStart() { - // This is called iff the delegate was initialized, so it's a good proxy for - // checking the initialization. - delegateInitialized = true + kosmos.runTest { + val factory = controllerFactory() + val runner = underTest.createLongLivedRunner(factory, testScope, forLaunch = false) + assertNull(runner.delegate) + + var delegateInitialized = false + underTest.addListener( + object : ActivityTransitionAnimator.Listener { + override fun onTransitionAnimationStart() { + // This is called iff the delegate was initialized, so it's a good proxy for + // checking the initialization. + delegateInitialized = true + } } - } - ) - runner.takeOverAnimation( - arrayOf(fakeWindow(MODE_CLOSING)), - arrayOf(WindowAnimationState()), - SurfaceControl.Transaction(), - iCallback, - ) + ) + runner.takeOverAnimation( + arrayOf(fakeWindow(MODE_CLOSING)), + arrayOf(WindowAnimationState()), + SurfaceControl.Transaction(), + iCallback, + ) + testScope.advanceUntilIdle() + waitForIdleSync() - waitForIdleSync() - assertTrue(delegateInitialized) + assertTrue(delegateInitialized) + } } @DisableFlags( @@ -442,7 +451,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { ) @Test fun animationTakeoverThrows_whenTheFlagsAreDisabled() { - val runner = activityTransitionAnimator.createEphemeralRunner(controller) + val runner = underTest.createEphemeralRunner(controller) assertThrows(IllegalStateException::class.java) { runner.takeOverAnimation( arrayOf(fakeWindow()), @@ -459,7 +468,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { ) @Test fun disposeRunner_delegateDereferenced() { - val runner = activityTransitionAnimator.createEphemeralRunner(controller) + val runner = underTest.createEphemeralRunner(controller) assertNotNull(runner.delegate) runner.dispose() waitForIdleSync() @@ -469,13 +478,13 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { @Test fun concurrentListenerModification_doesNotThrow() { // Need a second listener to trigger the concurrent modification. - activityTransitionAnimator.addListener(object : ActivityTransitionAnimator.Listener {}) + underTest.addListener(object : ActivityTransitionAnimator.Listener {}) `when`(listener.onTransitionAnimationStart()).thenAnswer { - activityTransitionAnimator.removeListener(listener) + underTest.removeListener(listener) listener } - val runner = activityTransitionAnimator.createEphemeralRunner(controller) + val runner = underTest.createEphemeralRunner(controller) runner.onAnimationStart( TRANSIT_NONE, arrayOf(fakeWindow()), @@ -494,7 +503,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { component: ComponentName? = mock(ComponentName::class.java), ): ActivityTransitionAnimator.ControllerFactory { return object : ActivityTransitionAnimator.ControllerFactory(cookie, component) { - override fun createController(forLaunch: Boolean) = + override suspend fun createController(forLaunch: Boolean) = object : DelegateTransitionAnimatorController(controller) { override val isLaunching: Boolean get() = forLaunch diff --git a/packages/SystemUI/tests/src/com/android/systemui/brightness/ui/compose/BrightnessSliderMotionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/brightness/ui/compose/BrightnessSliderMotionTest.kt new file mode 100644 index 000000000000..9dab9d735603 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/brightness/ui/compose/BrightnessSliderMotionTest.kt @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2025 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.brightness.ui.compose + +import android.platform.test.annotations.MotionTest +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.swipeLeft +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.android.compose.theme.PlatformTheme +import com.android.systemui.SysuiTestCase +import com.android.systemui.brightness.ui.viewmodel.BrightnessSliderViewModel +import com.android.systemui.common.shared.model.asIcon +import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.motion.createSysUiComposeMotionTestRule +import com.android.systemui.utils.PolicyRestriction +import kotlin.test.Test +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.joinAll +import org.junit.Rule +import org.junit.runner.RunWith +import platform.test.motion.compose.ComposeRecordingSpec +import platform.test.motion.compose.MotionControl +import platform.test.motion.compose.MotionControlScope +import platform.test.motion.compose.feature +import platform.test.motion.compose.motionTestValueOfNode +import platform.test.motion.compose.recordMotion +import platform.test.motion.compose.runTest +import platform.test.motion.compose.values.MotionTestValueKey +import platform.test.motion.golden.FeatureCapture +import platform.test.motion.golden.TimeSeriesCaptureScope +import platform.test.motion.golden.asDataPoint +import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.Displays.Phone + +@RunWith(AndroidJUnit4::class) +@LargeTest +@MotionTest +class BrightnessSliderMotionTest : SysuiTestCase() { + + private val deviceSpec = DeviceEmulationSpec(Phone) + private val kosmos = Kosmos() + + @get:Rule val motionTestRule = createSysUiComposeMotionTestRule(kosmos, deviceSpec) + + @Composable + private fun BrightnessSliderUnderTest(startingValue: Int) { + PlatformTheme { + BrightnessSlider( + gammaValue = startingValue, + modifier = Modifier.wrapContentHeight().fillMaxWidth(), + valueRange = 0..100, + iconResProvider = BrightnessSliderViewModel::getIconForPercentage, + imageLoader = { resId, context -> context.getDrawable(resId)!!.asIcon(null) }, + restriction = PolicyRestriction.NoRestriction, + onRestrictedClick = {}, + onDrag = {}, + onStop = {}, + overriddenByAppState = false, + hapticsViewModelFactory = kosmos.sliderHapticsViewModelFactory, + ) + } + } + + @Test + fun iconAlphaChanges() { + motionTestRule.runTest(timeout = 30.seconds) { + val motion = + recordMotion( + content = { BrightnessSliderUnderTest(100) }, + ComposeRecordingSpec( + MotionControl(delayReadyToPlay = { awaitCondition { !isAnimating } }) { + coroutineScope { + val gesture = async { + performTouchInputAsync( + onNode(hasTestTag("com.android.systemui:id/slider")) + ) { + swipeLeft(startX = right, endX = left, durationMillis = 500) + } + } + val animationEnd = async { + awaitCondition { isAnimating } + awaitCondition { !isAnimating } + } + joinAll(gesture, animationEnd) + } + } + ) { + featureFloat(BrightnessSliderMotionTestKeys.ActiveIconAlpha) + featureFloat(BrightnessSliderMotionTestKeys.InactiveIconAlpha) + }, + ) + assertThat(motion).timeSeriesMatchesGolden("brightnessSlider_iconAlphaChanges") + } + } + + private companion object { + + val MotionControlScope.isAnimating: Boolean + get() = motionTestValueOfNode(BrightnessSliderMotionTestKeys.AnimatingIcon) + + fun TimeSeriesCaptureScope<SemanticsNodeInteractionsProvider>.featureFloat( + motionTestValueKey: MotionTestValueKey<Float> + ) { + feature( + motionTestValueKey = motionTestValueKey, + capture = + FeatureCapture(motionTestValueKey.semanticsPropertyKey.name) { + it.asDataPoint() + }, + ) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/AmbientLightModeMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/AmbientLightModeMonitorTest.kt index 43ee388e44a7..8a2dc15d7545 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/AmbientLightModeMonitorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/AmbientLightModeMonitorTest.kt @@ -23,6 +23,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.util.sensors.AsyncSensorManager import java.util.Optional +import javax.inject.Provider import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -50,7 +51,11 @@ class AmbientLightModeMonitorTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) ambientLightModeMonitor = - AmbientLightModeMonitor(Optional.of(algorithm), sensorManager, Optional.of(sensor)) + AmbientLightModeMonitor( + Optional.of(algorithm), + sensorManager, + Optional.of(Provider { sensor }), + ) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java index 2715cb31ca8b..86094d1a0fef 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java @@ -891,6 +891,13 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { } @Test + public void getTransferableMediaDevice_triggersFromLocalMediaManager() { + mMediaSwitchingController.getTransferableMediaDevices(); + + verify(mLocalMediaManager).getTransferableMediaDevices(); + } + + @Test public void getDeselectableMediaDevice_triggersFromLocalMediaManager() { mMediaSwitchingController.getDeselectableMediaDevice(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt index 9abe9aa5e598..3de7db76deae 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt @@ -16,6 +16,8 @@ package com.android.systemui.shade +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View @@ -26,6 +28,7 @@ import androidx.annotation.IdRes import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.fragments.FragmentHostManager import com.android.systemui.fragments.FragmentService @@ -122,6 +125,7 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, SCRIM_MARGIN) overrideResource(R.dimen.notification_panel_margin_bottom, NOTIFICATIONS_MARGIN) + overrideResource(R.dimen.notification_2025_panel_margin_bottom, NOTIFICATIONS_MARGIN) overrideResource(R.bool.config_use_split_notification_shade, false) overrideResource(R.dimen.qs_footer_actions_bottom_padding, FOOTER_ACTIONS_PADDING) overrideResource(R.dimen.qs_footer_action_inset, FOOTER_ACTIONS_INSET) @@ -360,6 +364,7 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { } @Test + @DisableFlags(Flags.FLAG_NOTIFICATIONS_REDESIGN_FOOTER_VIEW) fun testNotificationsMarginBottomIsUpdated() { Mockito.clearInvocations(view) enableSplitShade() @@ -371,6 +376,18 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { } @Test + @EnableFlags(Flags.FLAG_NOTIFICATIONS_REDESIGN_FOOTER_VIEW) + fun testNotificationsMarginBottomIsUpdated_footerRedesign() { + Mockito.clearInvocations(view) + enableSplitShade() + verify(view).setNotificationsMarginBottom(NOTIFICATIONS_MARGIN) + + overrideResource(R.dimen.notification_2025_panel_margin_bottom, 100) + disableSplitShade() + verify(view).setNotificationsMarginBottom(100) + } + + @Test fun testSplitShadeLayout_qsFrameHasHorizontalMarginsOfZero() { enableSplitShade() underTest.updateResources() diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt index 4c12cc886e33..552fe8d5bc55 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt @@ -16,6 +16,8 @@ package com.android.systemui.shade +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View @@ -26,6 +28,7 @@ import androidx.annotation.IdRes import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.fragments.FragmentHostManager import com.android.systemui.fragments.FragmentService @@ -122,6 +125,7 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, SCRIM_MARGIN) overrideResource(R.dimen.notification_panel_margin_bottom, NOTIFICATIONS_MARGIN) + overrideResource(R.dimen.notification_2025_panel_margin_bottom, NOTIFICATIONS_MARGIN) overrideResource(R.bool.config_use_split_notification_shade, false) overrideResource(R.dimen.qs_footer_actions_bottom_padding, FOOTER_ACTIONS_PADDING) overrideResource(R.dimen.qs_footer_action_inset, FOOTER_ACTIONS_INSET) @@ -358,6 +362,7 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { } @Test + @DisableFlags(Flags.FLAG_NOTIFICATIONS_REDESIGN_FOOTER_VIEW) fun testNotificationsMarginBottomIsUpdated() { Mockito.clearInvocations(view) enableSplitShade() @@ -369,6 +374,18 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { } @Test + @EnableFlags(Flags.FLAG_NOTIFICATIONS_REDESIGN_FOOTER_VIEW) + fun testNotificationsMarginBottomIsUpdated_footerRedesign() { + Mockito.clearInvocations(view) + enableSplitShade() + verify(view).setNotificationsMarginBottom(NOTIFICATIONS_MARGIN) + + overrideResource(R.dimen.notification_2025_panel_margin_bottom, 100) + disableSplitShade() + verify(view).setNotificationsMarginBottom(100) + } + + @Test fun testSplitShadeLayout_isAlignedToGuideline() { enableSplitShade() underTest.updateResources() diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelKosmos.kt index 35e85bb1e68d..b6f8ec666001 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelKosmos.kt @@ -19,6 +19,7 @@ package com.android.systemui.brightness.ui.viewmodel import com.android.systemui.brightness.domain.interactor.brightnessPolicyEnforcementInteractor import com.android.systemui.brightness.domain.interactor.screenBrightnessInteractor import com.android.systemui.classifier.domain.interactor.falsingInteractor +import com.android.systemui.graphics.imageLoader import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory import com.android.systemui.kosmos.Kosmos import com.android.systemui.settings.brightness.domain.interactor.brightnessMirrorShowingInteractor @@ -36,6 +37,7 @@ val Kosmos.brightnessSliderViewModelFactory: BrightnessSliderViewModel.Factory b supportsMirroring = allowsMirroring, falsingInteractor = falsingInteractor, brightnessWarningToast = brightnessWarningToast, + imageLoader = imageLoader, ) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt index b3c1411243c1..3f35bb9f3520 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt @@ -17,8 +17,7 @@ import kotlinx.coroutines.launch /** Fake implementation of [CommunalSceneRepository]. */ class FakeCommunalSceneRepository( private val applicationScope: CoroutineScope, - override val currentScene: MutableStateFlow<SceneKey> = - MutableStateFlow(CommunalScenes.Default), + override val currentScene: MutableStateFlow<SceneKey> = MutableStateFlow(CommunalScenes.Default), ) : CommunalSceneRepository { override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) = @@ -31,6 +30,10 @@ class FakeCommunalSceneRepository( } } + override suspend fun showHubFromPowerButton() { + snapToScene(CommunalScenes.Communal) + } + private val defaultTransitionState = ObservableTransitionState.Idle(CommunalScenes.Default) private val _transitionState = MutableStateFlow<Flow<ObservableTransitionState>?>(null) override val transitionState: StateFlow<ObservableTransitionState> = diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorKosmos.kt index 13cbddff8803..3f07d05a4786 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.qs.tiles.impl.modes.domain.interactor import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.mainCoroutineContext import com.android.systemui.qs.tiles.base.actions.qsTileIntentUserInputHandler import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor import com.android.systemui.statusbar.policy.ui.dialog.modesDialogDelegate @@ -26,6 +27,7 @@ import javax.inject.Provider val Kosmos.modesTileUserActionInteractor: ModesTileUserActionInteractor by Kosmos.Fixture { ModesTileUserActionInteractor( + mainCoroutineContext, qsTileIntentUserInputHandler, Provider { modesDialogDelegate }.get(), zenModeInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt index 609f97d0b249..ae4e8d275341 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt @@ -4,6 +4,7 @@ import android.view.View import com.android.compose.animation.scene.ObservableTransitionState import com.android.systemui.classifier.domain.interactor.falsingInteractor import com.android.systemui.haptics.msdl.msdlPlayer +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.ui.viewmodel.lightRevealScrimViewModel import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture @@ -100,6 +101,7 @@ val Kosmos.sceneContainerViewModelFactory by Fixture { motionEventHandlerReceiver = motionEventHandlerReceiver, lightRevealScrim = lightRevealScrimViewModel, wallpaperViewModel = wallpaperViewModel, + keyguardInteractor = keyguardInteractor, ) } } diff --git a/services/core/java/com/android/server/TradeInModeService.java b/services/core/java/com/android/server/TradeInModeService.java index a69667395ebd..1a9e02c86560 100644 --- a/services/core/java/com/android/server/TradeInModeService.java +++ b/services/core/java/com/android/server/TradeInModeService.java @@ -41,12 +41,15 @@ import android.util.Slog; import java.io.FileWriter; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; public final class TradeInModeService extends SystemService { private static final String TAG = "TradeInModeService"; private static final String TIM_PROP = "persist.adb.tradeinmode"; + private static final String TIM_TEST_PROP = "persist.adb.test_tradeinmode"; private static final int TIM_STATE_UNSET = 0; @@ -108,6 +111,10 @@ public final class TradeInModeService extends SystemService { // setup completion observer. if (isDeviceSetup()) { stopTradeInMode(); + } else if (isDebuggable() && !isForceEnabledForTesting()) { + // The device was made debuggable after entering TIM. This + // can happen while flashing. For convenience, leave test mode. + leaveTestMode(); } else { watchForSetupCompletion(); watchForNetworkChange(); @@ -171,12 +178,7 @@ public final class TradeInModeService extends SystemService { Slog.e(TAG, "Cannot enter evaluation mode, FRP lock is present."); return false; } - - try (FileWriter fw = new FileWriter(WIPE_INDICATOR_FILE, - StandardCharsets.US_ASCII)) { - fw.write("0"); - } catch (IOException e) { - Slog.e(TAG, "Failed to write " + WIPE_INDICATOR_FILE, e); + if (!scheduleTradeInModeWipe()) { return false; } @@ -189,7 +191,7 @@ public final class TradeInModeService extends SystemService { } SystemProperties.set(TIM_PROP, Integer.toString(TIM_STATE_EVALUATION_MODE)); - SystemProperties.set("ctl.restart", "adbd"); + restartAdbd(); return true; } @@ -200,6 +202,55 @@ public final class TradeInModeService extends SystemService { "Cannot test for trade-in evaluation mode allowed"); return !isFrpActive(); } + + @Override + @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE) + public void scheduleWipeForTesting() { + enforceTestingPermissions(); + + scheduleTradeInModeWipe(); + } + + @Override + @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE) + public void startTesting() { + enforceTestingPermissions(); + + enterTestMode(); + } + + @Override + @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE) + public void stopTesting() { + enforceTestingPermissions(); + + if (!isForceEnabledForTesting()) { + throw new IllegalStateException("testing must have been started"); + } + + final long callingId = Binder.clearCallingIdentity(); + try { + leaveTestMode(); + } finally { + Binder.restoreCallingIdentity(callingId); + } + } + + @Override + @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE) + public boolean isTesting() { + enforceTestingPermissions(); + + return isForceEnabledForTesting(); + } + + private void enforceTestingPermissions() { + mContext.enforceCallingOrSelfPermission("android.permission.ENTER_TRADE_IN_MODE", + "Caller must have ENTER_TRADE_IN_MODE permission"); + if (!isDebuggable()) { + throw new SecurityException("ro.debuggable must be set to 1"); + } + } } private void startTradeInMode() { @@ -207,8 +258,7 @@ public final class TradeInModeService extends SystemService { SystemProperties.set(TIM_PROP, Integer.toString(TIM_STATE_FOYER)); - final ContentResolver cr = mContext.getContentResolver(); - Settings.Global.putInt(cr, Settings.Global.ADB_ENABLED, 1); + setAdbEnabled(true); watchForSetupCompletion(); watchForNetworkChange(); @@ -223,8 +273,51 @@ public final class TradeInModeService extends SystemService { removeNetworkWatch(); removeAccountsWatch(); + if (isForceEnabledForTesting()) { + // If testing in a debug build, we need to re-enable ADB. + restartAdbd(); + } else { + // Otherwise, ADB must not be enabled. + setAdbEnabled(false); + } + } + + private void enterTestMode() { + SystemProperties.set(TIM_TEST_PROP, "1"); + } + + private void leaveTestMode() { + if (getTradeInModeState() == TIM_STATE_FOYER) { + stopTradeInMode(); + } + + SystemProperties.set(TIM_TEST_PROP, ""); + SystemProperties.set(TIM_PROP, ""); + try { + Files.deleteIfExists(Paths.get(WIPE_INDICATOR_FILE)); + } catch (IOException e) { + Slog.e(TAG, "Failed to remove wipe indicator", e); + } + } + + private boolean scheduleTradeInModeWipe() { + try (FileWriter fw = new FileWriter(WIPE_INDICATOR_FILE, + StandardCharsets.US_ASCII)) { + fw.write("0"); + } catch (IOException e) { + Slog.e(TAG, "Failed to write " + WIPE_INDICATOR_FILE, e); + return false; + } + return true; + } + + private void restartAdbd() { + SystemProperties.set("ctl.restart", "adbd"); + } + + private void setAdbEnabled(boolean enabled) { final ContentResolver cr = mContext.getContentResolver(); - Settings.Global.putInt(cr, Settings.Global.ADB_ENABLED, 0); + Settings.Global.putInt(cr, Settings.Global.ADB_ENABLED, enabled ? 1 : 0); } private int getTradeInModeState() { @@ -236,7 +329,7 @@ public final class TradeInModeService extends SystemService { } private boolean isForceEnabledForTesting() { - return SystemProperties.getInt("persist.adb.test_tradeinmode", 0) == 1; + return isDebuggable() && SystemProperties.getInt(TIM_TEST_PROP, 0) == 1; } private boolean isAdbEnabled() { diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index b5f11e8a0f78..c6338307b192 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -189,6 +189,7 @@ public class SettingsToPropertiesMapper { "desktop_connectivity", "desktop_hwsec", "desktop_stats", + "desktop_wifi", "devoptions_settings", "game", "gpu", diff --git a/services/core/java/com/android/server/audio/AudioManagerShellCommand.java b/services/core/java/com/android/server/audio/AudioManagerShellCommand.java index fece7a899f0a..ae961b53f547 100644 --- a/services/core/java/com/android/server/audio/AudioManagerShellCommand.java +++ b/services/core/java/com/android/server/audio/AudioManagerShellCommand.java @@ -83,6 +83,8 @@ class AudioManagerShellCommand extends ShellCommand { return setGroupVolume(); case "adj-group-volume": return adjGroupVolume(); + case "set-hardening": + return setEnableHardening(); } return 0; } @@ -130,6 +132,8 @@ class AudioManagerShellCommand extends ShellCommand { pw.println(" Sets the volume for GROUP_ID to VOLUME_INDEX"); pw.println(" adj-group-volume GROUP_ID <RAISE|LOWER|MUTE|UNMUTE>"); pw.println(" Adjusts the group volume for GROUP_ID given the specified direction"); + pw.println(" set-enable-hardening <1|0>"); + pw.println(" Enables full audio hardening enforcement, disabling any exemptions"); } private int setSurroundFormatEnabled() { @@ -405,6 +409,20 @@ class AudioManagerShellCommand extends ShellCommand { return 0; } + private int setEnableHardening() { + final Context context = mService.mContext; + final AudioManager am = context.getSystemService(AudioManager.class); + final boolean shouldEnable = !(readIntArg() == 0); + getOutPrintWriter().println( + "calling AudioManager.setEnableHardening(" + shouldEnable + ")"); + try { + am.setEnableHardening(shouldEnable); + } catch (Exception e) { + getOutPrintWriter().println("Exception: " + e); + } + return 0; + } + private int readIntArg() throws IllegalArgumentException { final String argText = getNextArg(); diff --git a/services/core/java/com/android/server/audio/AudioPolicyFacade.java b/services/core/java/com/android/server/audio/AudioPolicyFacade.java index f652b33b3fd3..6c0b81f513be 100644 --- a/services/core/java/com/android/server/audio/AudioPolicyFacade.java +++ b/services/core/java/com/android/server/audio/AudioPolicyFacade.java @@ -26,4 +26,5 @@ public interface AudioPolicyFacade { public boolean isHotwordStreamSupported(boolean lookbackAudio); public INativePermissionController getPermissionController(); public void registerOnStartTask(Runnable r); + public void setEnableHardening(boolean shouldEnable); } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index f2830090e7db..02e0d9ffb1d4 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -781,7 +781,8 @@ public class AudioService extends IAudioService.Stub private int mRingerModeExternal = -1; // reported ringer mode to outside clients (AudioManager) /** @see System#MODE_RINGER_STREAMS_AFFECTED */ - private int mRingerModeAffectedStreams = 0; + @VisibleForTesting + protected int mRingerModeAffectedStreams = 0; private int mZenModeAffectedStreams = 0; @@ -1191,6 +1192,11 @@ public class AudioService extends IAudioService.Stub private @AttributeSystemUsage int[] mSupportedSystemUsages = new int[]{AudioAttributes.USAGE_CALL_ASSISTANT}; + // Tracks the API/shell override of hardening enforcement used for debugging + // When this is set to true, enforcement is on regardless of flag state and any specific + // exemptions in place for compat purposes. + private final AtomicBoolean mShouldEnableAllHardening = new AtomicBoolean(false); + // Defines the format for the connection "address" for ALSA devices public static String makeAlsaAddressString(int card, int device) { return "card=" + card + ";device=" + device; @@ -1334,6 +1340,10 @@ public class AudioService extends IAudioService.Stub mAudioVolumeGroupHelper = audioVolumeGroupHelper; mSettings = settings; mAudioPolicy = audioPolicy; + mAudioPolicy.registerOnStartTask(() -> { + mAudioPolicy.setEnableHardening(mShouldEnableAllHardening.get()); + }); + mPlatformType = AudioSystem.getPlatformType(context); mBroadcastHandlerThread = new HandlerThread("AudioService Broadcast"); @@ -6315,17 +6325,15 @@ public class AudioService extends IAudioService.Stub } } sRingerAndZenModeMutedStreams &= ~(1 << streamType); - sMuteLogger.enqueue(new AudioServiceEvents.RingerZenMutedStreamsEvent( - sRingerAndZenModeMutedStreams, "muteRingerModeStreams")); vss.mute(false, "muteRingerModeStreams"); } else { // mute sRingerAndZenModeMutedStreams |= (1 << streamType); - sMuteLogger.enqueue(new AudioServiceEvents.RingerZenMutedStreamsEvent( - sRingerAndZenModeMutedStreams, "muteRingerModeStreams")); vss.mute(true, "muteRingerModeStreams"); } } + sMuteLogger.enqueue(new AudioServiceEvents.RingerZenMutedStreamsEvent( + sRingerAndZenModeMutedStreams, "muteRingerModeStreams")); } private boolean isAlarm(int streamType) { @@ -10045,12 +10053,14 @@ public class AudioService extends IAudioService.Stub new AudioServiceEvents.StreamMuteEvent(mStreamType, state, src)); // check to see if unmuting should not have happened due to ringer muted streams if (!state && isStreamMutedByRingerOrZenMode(mStreamType)) { - Log.e(TAG, "Unmuting stream " + mStreamType + Slog.e(TAG, "Attempt to unmute stream " + mStreamType + " despite ringer-zen muted stream 0x" + Integer.toHexString(AudioService.sRingerAndZenModeMutedStreams), new Exception()); // this will put a stack trace in the logs sMuteLogger.enqueue(new AudioServiceEvents.StreamUnmuteErrorEvent( mStreamType, AudioService.sRingerAndZenModeMutedStreams)); + // do not change mute state + return false; } mIsMuted = state; if (apply) { @@ -15019,6 +15029,16 @@ public class AudioService extends IAudioService.Stub return true; } + /** + * @see AudioManager#setEnableHardening(boolean) + */ + @android.annotation.EnforcePermission(MODIFY_AUDIO_SETTINGS_PRIVILEGED) + public void setEnableHardening(boolean shouldEnable) { + super.setEnableHardening_enforcePermission(); + mShouldEnableAllHardening.set(shouldEnable); + mAudioPolicy.setEnableHardening(shouldEnable); + } + //====================== // Audioserver state dispatch //====================== diff --git a/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java b/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java index 09701e49a8ac..c41f41e0f31b 100644 --- a/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java +++ b/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java @@ -80,4 +80,14 @@ public class DefaultAudioPolicyFacade implements AudioPolicyFacade { public void registerOnStartTask(Runnable task) { mServiceHolder.registerOnStartTask(unused -> task.run()); } + + @Override + public void setEnableHardening(boolean shouldEnable) { + IAudioPolicyService ap = mServiceHolder.waitForService(); + try { + ap.setEnableHardening(shouldEnable); + } catch (RemoteException e) { + mServiceHolder.attemptClear(ap.asBinder()); + } + } } diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 49b451a83efa..e83efc573ea8 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -107,6 +107,7 @@ import android.hardware.display.DisplayManagerInternal; import android.hardware.display.DisplayManagerInternal.DisplayGroupListener; import android.hardware.display.DisplayManagerInternal.DisplayTransactionListener; import android.hardware.display.DisplayTopology; +import android.hardware.display.DisplayTopologyGraph; import android.hardware.display.DisplayViewport; import android.hardware.display.DisplayedContentSample; import android.hardware.display.DisplayedContentSamplingAttributes; @@ -292,6 +293,7 @@ public final class DisplayManagerService extends SystemService { private final DisplayModeDirector mDisplayModeDirector; private final ExternalDisplayPolicy mExternalDisplayPolicy; private WindowManagerInternal mWindowManagerInternal; + @Nullable private InputManagerInternal mInputManagerInternal; private ActivityManagerInternal mActivityManagerInternal; private final UidImportanceListener mUidImportanceListener = new UidImportanceListener(); @@ -680,8 +682,15 @@ public final class DisplayManagerService extends SystemService { mExternalDisplayPolicy = new ExternalDisplayPolicy(new ExternalDisplayPolicyInjector()); if (mFlags.isDisplayTopologyEnabled()) { final var backupManager = new BackupManager(mContext); + Consumer<Pair<DisplayTopology, DisplayTopologyGraph>> topologyChangedCallback = + update -> { + if (mInputManagerInternal != null) { + mInputManagerInternal.setDisplayTopology(update.second); + } + deliverTopologyUpdate(update.first); + }; mDisplayTopologyCoordinator = new DisplayTopologyCoordinator( - this::isExtendedDisplayEnabled, this::deliverTopologyUpdate, + this::isExtendedDisplayEnabled, topologyChangedCallback, new HandlerExecutor(mHandler), mSyncRoot, backupManager::dataChanged); } else { mDisplayTopologyCoordinator = null; diff --git a/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java b/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java index c73ab32f117a..fbfe85cd3b78 100644 --- a/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java +++ b/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java @@ -19,8 +19,11 @@ package com.android.server.display; import static android.hardware.display.DisplayTopology.pxToDp; import android.hardware.display.DisplayTopology; +import android.hardware.display.DisplayTopologyGraph; +import android.util.Pair; import android.util.Slog; import android.util.SparseArray; +import android.util.SparseIntArray; import android.view.Display; import android.view.DisplayInfo; @@ -53,6 +56,12 @@ class DisplayTopologyCoordinator { @GuardedBy("mSyncRoot") private DisplayTopology mTopology; + + // Map from logical display ID to logical display density. Should always be consistent with + // mTopology. + @GuardedBy("mSyncRoot") + private final SparseIntArray mDensities = new SparseIntArray(); + @GuardedBy("mSyncRoot") private final Map<String, Integer> mUniqueIdToDisplayIdMapping = new HashMap<>(); @@ -69,13 +78,13 @@ class DisplayTopologyCoordinator { * Should be invoked from the corresponding executor. * A copy of the topology should be sent that will not be modified by the system. */ - private final Consumer<DisplayTopology> mOnTopologyChangedCallback; + private final Consumer<Pair<DisplayTopology, DisplayTopologyGraph>> mOnTopologyChangedCallback; private final Executor mTopologyChangeExecutor; private final DisplayManagerService.SyncRoot mSyncRoot; private final Runnable mTopologySavedCallback; DisplayTopologyCoordinator(BooleanSupplier isExtendedDisplayEnabled, - Consumer<DisplayTopology> onTopologyChangedCallback, + Consumer<Pair<DisplayTopology, DisplayTopologyGraph>> onTopologyChangedCallback, Executor topologyChangeExecutor, DisplayManagerService.SyncRoot syncRoot, Runnable topologySavedCallback) { this(new Injector(), isExtendedDisplayEnabled, onTopologyChangedCallback, @@ -84,7 +93,7 @@ class DisplayTopologyCoordinator { @VisibleForTesting DisplayTopologyCoordinator(Injector injector, BooleanSupplier isExtendedDisplayEnabled, - Consumer<DisplayTopology> onTopologyChangedCallback, + Consumer<Pair<DisplayTopology, DisplayTopologyGraph>> onTopologyChangedCallback, Executor topologyChangeExecutor, DisplayManagerService.SyncRoot syncRoot, Runnable topologySavedCallback) { mTopology = injector.getTopology(); @@ -107,7 +116,7 @@ class DisplayTopologyCoordinator { } synchronized (mSyncRoot) { addDisplayIdMappingLocked(info); - + mDensities.put(info.displayId, info.logicalDensityDpi); mTopology.addDisplay(info.displayId, getWidth(info), getHeight(info)); restoreTopologyLocked(); sendTopologyUpdateLocked(); @@ -119,7 +128,13 @@ class DisplayTopologyCoordinator { * @param info The new display info */ void onDisplayChanged(DisplayInfo info) { + if (!isDisplayAllowedInTopology(info)) { + return; + } synchronized (mSyncRoot) { + if (mDensities.indexOfKey(info.displayId) >= 0) { + mDensities.put(info.displayId, info.logicalDensityDpi); + } if (mTopology.updateDisplay(info.displayId, getWidth(info), getHeight(info))) { sendTopologyUpdateLocked(); } @@ -132,6 +147,7 @@ class DisplayTopologyCoordinator { */ void onDisplayRemoved(int displayId) { synchronized (mSyncRoot) { + mDensities.delete(displayId); if (mTopology.removeDisplay(displayId)) { removeDisplayIdMappingLocked(displayId); restoreTopologyLocked(); @@ -256,7 +272,9 @@ class DisplayTopologyCoordinator { @GuardedBy("mSyncRoot") private void sendTopologyUpdateLocked() { DisplayTopology copy = mTopology.copy(); - mTopologyChangeExecutor.execute(() -> mOnTopologyChangedCallback.accept(copy)); + SparseIntArray densities = mDensities.clone(); + mTopologyChangeExecutor.execute(() -> mOnTopologyChangedCallback.accept( + new Pair<>(copy, copy.getGraph(densities)))); } @VisibleForTesting diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java index b13dee530ee2..b529853c63a4 100644 --- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java +++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java @@ -58,6 +58,8 @@ import java.util.stream.Stream; private static final String UNIQUE_SYSTEM_ID_PREFIX = "SYSTEM"; private static final String UNIQUE_SYSTEM_ID_SEPARATOR = "-"; + private static final boolean FORCE_GLOBAL_ROUTING_SESSION = true; + private static final String PACKAGE_NAME_FOR_GLOBAL_SESSION = ""; private final PackageManager mPackageManager; @@ -118,6 +120,9 @@ import java.util.stream.Stream; String routeOriginalId, int transferReason) { synchronized (mLock) { + if (FORCE_GLOBAL_ROUTING_SESSION) { + clientPackageName = PACKAGE_NAME_FOR_GLOBAL_SESSION; + } var targetProviderProxyId = mOriginalRouteIdToProviderId.get(routeOriginalId); var targetProviderProxyRecord = mProxyRecords.get(targetProviderProxyId); // Holds the target route, if it's managed by a provider service. Holds null otherwise. @@ -125,7 +130,7 @@ import java.util.stream.Stream; targetProviderProxyRecord != null ? targetProviderProxyRecord.getRouteByOriginalId(routeOriginalId) : null; - var existingSessionRecord = mPackageNameToSessionRecord.get(clientPackageName); + var existingSessionRecord = getSessionRecordByPackageName(clientPackageName); if (existingSessionRecord != null) { var existingSession = existingSessionRecord.mSourceSessionInfo; if (targetProviderProxyId != null @@ -206,7 +211,7 @@ import java.util.stream.Stream; if (systemSession == null) { return null; } - var overridingSession = mPackageNameToSessionRecord.get(packageName); + var overridingSession = getSessionRecordByPackageName(packageName); if (overridingSession != null) { var builder = new RoutingSessionInfo.Builder(overridingSession.mTranslatedSessionInfo) @@ -251,7 +256,7 @@ import java.util.stream.Stream; return; } synchronized (mLock) { - var sessionRecord = mSessionOriginalIdToSessionRecord.get(sessionOriginalId); + var sessionRecord = getSessionRecordByOriginalId(sessionOriginalId); var proxyRecord = sessionRecord != null ? sessionRecord.getProxyRecord() : null; if (proxyRecord != null) { proxyRecord.mProxy.setSessionVolume( @@ -262,6 +267,23 @@ import java.util.stream.Stream; notifyRequestFailed(requestId, MediaRoute2ProviderService.REASON_ROUTE_NOT_AVAILABLE); } + @GuardedBy("mLock") + private SystemMediaSessionRecord getSessionRecordByOriginalId(String sessionOriginalId) { + if (FORCE_GLOBAL_ROUTING_SESSION) { + return getSessionRecordByPackageName(PACKAGE_NAME_FOR_GLOBAL_SESSION); + } else { + return mSessionOriginalIdToSessionRecord.get(sessionOriginalId); + } + } + + @GuardedBy("mLock") + private SystemMediaSessionRecord getSessionRecordByPackageName(String clientPackageName) { + if (FORCE_GLOBAL_ROUTING_SESSION) { + clientPackageName = PACKAGE_NAME_FOR_GLOBAL_SESSION; + } + return mPackageNameToSessionRecord.get(clientPackageName); + } + /** * Returns the uid that corresponds to the given name and user handle, or {@link * Process#INVALID_UID} if a uid couldn't be found. @@ -319,16 +341,34 @@ import java.util.stream.Stream; */ private void updateSessionInfo() { synchronized (mLock) { - var systemSessionInfo = mSystemSessionInfo; - if (systemSessionInfo == null) { + var globalSessionInfoRecord = + getSessionRecordByPackageName(PACKAGE_NAME_FOR_GLOBAL_SESSION); + var globalSessionInfo = + globalSessionInfoRecord != null + ? globalSessionInfoRecord.mTranslatedSessionInfo + : null; + if (globalSessionInfo == null) { + globalSessionInfo = mSystemSessionInfo; + } + if (globalSessionInfo == null) { // The system session info hasn't been initialized yet. Do nothing. return; } - var builder = new RoutingSessionInfo.Builder(systemSessionInfo); - mProxyRecords.values().stream() - .flatMap(ProviderProxyRecord::getRoutesStream) - .map(MediaRoute2Info::getOriginalId) - .forEach(builder::addTransferableRoute); + var builder = new RoutingSessionInfo.Builder(globalSessionInfo); + if (globalSessionInfo == mSystemSessionInfo) { + // The session is the system one. So we make all the service-provided routes + // available for transfer. The system transferable routes are already there. + mProxyRecords.values().stream() + .flatMap(ProviderProxyRecord::getRoutesStream) + .map(MediaRoute2Info::getOriginalId) + .forEach(builder::addTransferableRoute); + } else { + // The session is service-provided. So we add the system-provided routes as + // transferable. + mLastSystemProviderInfo.getRoutes().stream() + .map(MediaRoute2Info::getOriginalId) + .forEach(builder::addTransferableRoute); + } mSessionInfos.clear(); mSessionInfos.add(builder.build()); for (var sessionRecords : mPackageNameToSessionRecord.values()) { diff --git a/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java b/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java index 1b7c7ad94dc9..c0441e4e4d46 100644 --- a/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java +++ b/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java @@ -155,11 +155,14 @@ public class AndroidPackageUtils { public static NativeLibraryHelper.Handle createNativeLibraryHandle(AndroidPackage pkg) throws IOException { + boolean pageSizeCompatDisabled = pkg.getPageSizeAppCompatFlags() + == ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_MANIFEST_OVERRIDE_DISABLED; return NativeLibraryHelper.Handle.create( AndroidPackageUtils.getAllCodePaths(pkg), pkg.isMultiArch(), pkg.isExtractNativeLibrariesRequested(), - pkg.isDebuggable() + pkg.isDebuggable(), + pageSizeCompatDisabled ); } diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java index b905041b59e5..5c5a9c1b6c05 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java @@ -271,8 +271,9 @@ public class PermissionManagerService extends IPermissionManager.Stub { @NonNull String permissionName, int deviceId) { Objects.requireNonNull(permissionName, "permission can't be null."); Objects.requireNonNull(packageName, "package name can't be null."); + return mPermissionManagerServiceImpl.getPermissionRequestState(packageName, permissionName, - getPersistentDeviceId(deviceId)); + deviceId, getPersistentDeviceId(deviceId)); } private String getPersistentDeviceId(int deviceId) { diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java index ca70bddc5ac1..e51ec04e60fe 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java @@ -1014,7 +1014,8 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt } @Override - public int getPermissionRequestState(String packageName, String permName, String deviceId) { + public int getPermissionRequestState(String packageName, String permName, int deviceId, + String persistentDeviceId) { throw new IllegalStateException("getPermissionRequestState is not supported."); } diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java index b607832767a1..3d295f773805 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java @@ -415,7 +415,7 @@ public interface PermissionManagerServiceInterface extends PermissionManagerInte * for permission request permission flow. */ int getPermissionRequestState(@NonNull String packageName, @NonNull String permName, - @NonNull String deviceId); + int deviceId, @NonNull String persistentDeviceId); /** * Gets the permission states for requested package, persistent device and user. diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceLoggingDecorator.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceLoggingDecorator.java index ba5e97e7b113..f5764006e766 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceLoggingDecorator.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceLoggingDecorator.java @@ -247,10 +247,12 @@ public class PermissionManagerServiceLoggingDecorator implements PermissionManag } @Override - public int getPermissionRequestState(String packageName, String permName, String deviceId) { + public int getPermissionRequestState(String packageName, String permName, int deviceId, + String persistentDeviceId) { Log.i(LOG_TAG, "checkUidPermissionState(permName = " + permName + ", deviceId = " - + deviceId + ", packageName = " + packageName + ")"); - return mService.getPermissionRequestState(packageName, permName, deviceId); + + persistentDeviceId + ", packageName = " + packageName + ")"); + return mService.getPermissionRequestState( + packageName, permName, deviceId, persistentDeviceId); } @Override diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTestingShim.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTestingShim.java index 008c14db8b65..21a357025cfb 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTestingShim.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTestingShim.java @@ -319,8 +319,10 @@ public class PermissionManagerServiceTestingShim implements PermissionManagerSer } @Override - public int getPermissionRequestState(String packageName, String permName, String deviceId) { - return mNewImplementation.getPermissionRequestState(packageName, permName, deviceId); + public int getPermissionRequestState(String packageName, String permName, int deviceId, + String persistentDeviceId) { + return mNewImplementation.getPermissionRequestState( + packageName, permName, deviceId, persistentDeviceId); } @Override diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTracingDecorator.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTracingDecorator.java index 2a47f51da951..e51afb0f66c5 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTracingDecorator.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTracingDecorator.java @@ -347,11 +347,13 @@ public class PermissionManagerServiceTracingDecorator implements PermissionManag @Override - public int getPermissionRequestState(String packageName, String permName, String deviceId) { + public int getPermissionRequestState(String packageName, String permName, int deviceId, + String persistentDeviceId) { Trace.traceBegin(TRACE_TAG, "TaggedTracingPermissionManagerServiceImpl#checkUidPermissionState"); try { - return mService.getPermissionRequestState(packageName, permName, deviceId); + return mService.getPermissionRequestState( + packageName, permName, deviceId, persistentDeviceId); } finally { Trace.traceEnd(TRACE_TAG); } diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index 090707db50a5..8fae875eb29b 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -5979,10 +5979,19 @@ public final class PowerManagerService extends SystemService if (uids != null) { ws = new WorkSource(); - // XXX should WorkSource have a way to set uids as an int[] instead of adding them - // one at a time? - for (int uid : uids) { - ws.add(uid); + if (mFeatureFlags.isWakelockAttributionViaWorkchainEnabled()) { + int callingUid = Binder.getCallingUid(); + for (int uid : uids) { + WorkChain workChain = ws.createWorkChain(); + workChain.addNode(uid, null); + workChain.addNode(callingUid, null); + } + } else { + // XXX should WorkSource have a way to set uids as an int[] instead of + // adding them one at a time? + for (int uid : uids) { + ws.add(uid); + } } } updateWakeLockWorkSource(lock, ws, null); diff --git a/services/core/java/com/android/server/power/feature/PowerManagerFlags.java b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java index 42b44013bea2..ebc50fd85f24 100644 --- a/services/core/java/com/android/server/power/feature/PowerManagerFlags.java +++ b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java @@ -63,6 +63,10 @@ public class PowerManagerFlags { private final FlagState mMoveWscLoggingToNotifier = new FlagState(Flags.FLAG_MOVE_WSC_LOGGING_TO_NOTIFIER, Flags::moveWscLoggingToNotifier); + private final FlagState mWakelockAttributionViaWorkchain = + new FlagState(Flags.FLAG_WAKELOCK_ATTRIBUTION_VIA_WORKCHAIN, + Flags::wakelockAttributionViaWorkchain); + /** Returns whether early-screen-timeout-detector is enabled on not. */ public boolean isEarlyScreenTimeoutDetectorEnabled() { return mEarlyScreenTimeoutDetectorFlagState.isEnabled(); @@ -110,6 +114,13 @@ public class PowerManagerFlags { } /** + * @return Whether the wakelock attribution via workchain is enabled + */ + public boolean isWakelockAttributionViaWorkchainEnabled() { + return mWakelockAttributionViaWorkchain.isEnabled(); + } + + /** * dumps all flagstates * @param pw printWriter */ @@ -120,6 +131,7 @@ public class PowerManagerFlags { pw.println(" " + mPerDisplayWakeByTouch); pw.println(" " + mFrameworkWakelockInfo); pw.println(" " + mMoveWscLoggingToNotifier); + pw.println(" " + mWakelockAttributionViaWorkchain); } private static class FlagState { diff --git a/services/core/java/com/android/server/power/feature/power_flags.aconfig b/services/core/java/com/android/server/power/feature/power_flags.aconfig index 613daf820e34..fefe195dc337 100644 --- a/services/core/java/com/android/server/power/feature/power_flags.aconfig +++ b/services/core/java/com/android/server/power/feature/power_flags.aconfig @@ -23,6 +23,17 @@ flag { } flag { + name: "wakelock_attribution_via_workchain" + namespace: "power" + description: "Enables the attribution of wakelocks via WorkChain for updateWakelockUids" + bug: "331304805" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "improve_wakelock_latency" namespace: "power" description: "Feature flag for tracking the optimizations to improve the latency of acquiring and releasing a wakelock." diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index e8498bca1809..f38394e2a259 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -46,7 +46,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.app.WindowConfiguration.activityTypeToString; -import static android.app.WindowConfiguration.isFloating; import static android.app.admin.DevicePolicyResources.Drawables.Source.PROFILE_SWITCH_ANIMATION; import static android.app.admin.DevicePolicyResources.Drawables.Style.OUTLINE; import static android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_ICON; @@ -5605,7 +5604,6 @@ final class ActivityRecord extends WindowToken { } mAtmService.mBackNavigationController.onAppVisibilityChanged(this, visible); - onChildVisibilityRequested(visible); final DisplayContent displayContent = getDisplayContent(); displayContent.mOpeningApps.remove(this); @@ -8378,7 +8376,6 @@ final class ActivityRecord extends WindowToken { mConfigurationSeq = Math.max(++mConfigurationSeq, 1); getResolvedOverrideConfiguration().seq = mConfigurationSeq; - // TODO(b/392069771): Move to AppCompatSandboxingPolicy. // Sandbox max bounds by setting it to the activity bounds, if activity is letterboxed, or // has or will have mAppCompatDisplayInsets for size compat. Also forces an activity to be // sandboxed or not depending upon the configuration settings. @@ -8407,20 +8404,6 @@ final class ActivityRecord extends WindowToken { resolvedConfig.windowConfiguration.setMaxBounds(mTmpBounds); } - // Sandbox activity bounds in freeform to app bounds to force app to display within the - // container. This prevents UI cropping when activities can draw below insets which are - // normally excluded from appBounds before targetSDK < 35 - // (see ConfigurationContainer#applySizeOverrideIfNeeded). - if (isFloating(parentWindowingMode)) { - Rect appBounds = resolvedConfig.windowConfiguration.getAppBounds(); - if (appBounds == null || appBounds.isEmpty()) { - // When there is no override bounds, the activity will inherit the bounds from - // parent. - appBounds = mResolveConfigHint.mParentAppBoundsOverride; - } - resolvedConfig.windowConfiguration.setBounds(appBounds); - } - applySizeOverrideIfNeeded( mDisplayContent, info.applicationInfo, diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index 0d88a9b1f8ee..6a5adca91e39 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -162,6 +162,7 @@ import com.android.server.companion.virtual.VirtualDeviceManagerInternal; import com.android.server.pm.SaferIntentUtils; import com.android.server.utils.Slogf; import com.android.server.wm.ActivityMetricsLogger.LaunchingState; +import com.android.window.flags.Flags; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -278,7 +279,7 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { /** Helper for {@link Task#fillTaskInfo}. */ final TaskInfoHelper mTaskInfoHelper = new TaskInfoHelper(); - final OpaqueActivityHelper mOpaqueActivityHelper = new OpaqueActivityHelper(); + final OpaqueContainerHelper mOpaqueContainerHelper = new OpaqueContainerHelper(); private final ActivityTaskSupervisorHandler mHandler; final Looper mLooper; @@ -2913,41 +2914,90 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { } } - /** The helper to get the top opaque activity of a container. */ - static class OpaqueActivityHelper implements Predicate<ActivityRecord> { + /** The helper to calculate whether a container is opaque. */ + static class OpaqueContainerHelper implements Predicate<ActivityRecord> { private ActivityRecord mStarting; - private boolean mIncludeInvisibleAndFinishing; + private boolean mIgnoringInvisibleActivity; private boolean mIgnoringKeyguard; - ActivityRecord getOpaqueActivity(@NonNull WindowContainer<?> container) { - mIncludeInvisibleAndFinishing = true; - mIgnoringKeyguard = true; - return container.getActivity(this, - true /* traverseTopToBottom */, null /* boundary */); + /** Whether the container is opaque. */ + boolean isOpaque(@NonNull WindowContainer<?> container) { + return isOpaque(container, null /* starting */, true /* ignoringKeyguard */, + false /* ignoringInvisibleActivity */); } - ActivityRecord getVisibleOpaqueActivity( + /** + * Whether the container is opaque, but only including visible activities in its + * calculation. + */ + boolean isOpaque( @NonNull WindowContainer<?> container, @Nullable ActivityRecord starting, - boolean ignoringKeyguard) { + boolean ignoringKeyguard, boolean ignoringInvisibleActivity) { mStarting = starting; - mIncludeInvisibleAndFinishing = false; + mIgnoringInvisibleActivity = ignoringInvisibleActivity; mIgnoringKeyguard = ignoringKeyguard; - final ActivityRecord opaque = container.getActivity(this, - true /* traverseTopToBottom */, null /* boundary */); + + final boolean isOpaque; + if (!Flags.enableMultipleDesktopsBackend()) { + isOpaque = container.getActivity(this, + true /* traverseTopToBottom */, null /* boundary */) != null; + } else { + isOpaque = isOpaqueInner(container); + } mStarting = null; - return opaque; + return isOpaque; + } + + private boolean isOpaqueInner(@NonNull WindowContainer<?> container) { + // If it's a leaf task fragment, then opacity is calculated based on its activities. + if (container.asTaskFragment() != null + && ((TaskFragment) container).isLeafTaskFragment()) { + return container.getActivity(this, + true /* traverseTopToBottom */, null /* boundary */) != null; + } + // When not a leaf, it's considered opaque if any of its opaque children fill this + // container, unless the children are adjacent fragments, in which case as long as they + // are all opaque then |container| is also considered opaque, even if the adjacent + // task fragment aren't filling. + for (int i = 0; i < container.getChildCount(); i++) { + final WindowContainer<?> child = container.getChildAt(i); + if (child.fillsParent() && isOpaque(child)) { + return true; + } + + if (child.asTaskFragment() != null + && child.asTaskFragment().hasAdjacentTaskFragment()) { + final boolean isAnyTranslucent; + if (Flags.allowMultipleAdjacentTaskFragments()) { + final TaskFragment.AdjacentSet set = + child.asTaskFragment().getAdjacentTaskFragments(); + isAnyTranslucent = set.forAllTaskFragments( + tf -> !isOpaque(tf), null); + } else { + final TaskFragment adjacent = child.asTaskFragment() + .getAdjacentTaskFragment(); + isAnyTranslucent = !isOpaque(child) || !isOpaque(adjacent); + } + if (!isAnyTranslucent) { + // This task fragment and all its adjacent task fragments are opaque, + // consider it opaque even if it doesn't fill its parent. + return true; + } + } + } + return false; } @Override public boolean test(ActivityRecord r) { - if (!mIncludeInvisibleAndFinishing && r != mStarting + if (mIgnoringInvisibleActivity && r != mStarting && ((mIgnoringKeyguard && !r.visibleIgnoringKeyguard) || (!mIgnoringKeyguard && !r.isVisible()))) { // Ignore invisible activities that are not the currently starting activity // (about to be visible). return false; } - return r.occludesParent(mIncludeInvisibleAndFinishing /* includingFinishing */); + return r.occludesParent(!mIgnoringInvisibleActivity /* includingFinishing */); } } diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java index 0a2f6852f6e6..d5fe056a2ba4 100644 --- a/services/core/java/com/android/server/wm/AppTransitionController.java +++ b/services/core/java/com/android/server/wm/AppTransitionController.java @@ -80,7 +80,6 @@ import android.os.Trace; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Pair; -import android.view.Display; import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationDefinition; import android.view.WindowManager; @@ -252,7 +251,6 @@ public class AppTransitionController { // Check if there is any override if (!overrideWithTaskFragmentRemoteAnimation(transit, activityTypes)) { // Unfreeze the windows that were previously frozen for TaskFragment animation. - unfreezeEmbeddedChangingWindows(); overrideWithRemoteAnimationIfSet(animLpActivity, transit, activityTypes); } @@ -545,16 +543,6 @@ public class AppTransitionController { : null; } - private void unfreezeEmbeddedChangingWindows() { - final ArraySet<WindowContainer> changingContainers = mDisplayContent.mChangingContainers; - for (int i = changingContainers.size() - 1; i >= 0; i--) { - final WindowContainer wc = changingContainers.valueAt(i); - if (wc.isEmbedded()) { - wc.mSurfaceFreezer.unfreeze(wc.getSyncTransaction()); - } - } - } - private boolean transitionMayContainNonAppWindows(@TransitionOldType int transit) { // We don't want to have the client to animate any non-app windows. // Having {@code transit} of those types doesn't mean it will contain non-app windows, but diff --git a/services/core/java/com/android/server/wm/SurfaceAnimator.java b/services/core/java/com/android/server/wm/SurfaceAnimator.java index d7b6d96c781d..3dfff39e9b68 100644 --- a/services/core/java/com/android/server/wm/SurfaceAnimator.java +++ b/services/core/java/com/android/server/wm/SurfaceAnimator.java @@ -60,8 +60,6 @@ public class SurfaceAnimator { @VisibleForTesting SurfaceControl mLeash; @VisibleForTesting - SurfaceFreezer.Snapshot mSnapshot; - @VisibleForTesting final Animatable mAnimatable; @VisibleForTesting final OnAnimationFinishedCallback mInnerAnimationFinishedCallback; @@ -165,7 +163,7 @@ public class SurfaceAnimator { @AnimationType int type, @Nullable OnAnimationFinishedCallback animationFinishedCallback, @Nullable Runnable animationCancelledCallback, - @Nullable AnimationAdapter snapshotAnim, @Nullable SurfaceFreezer freezer) { + @Nullable AnimationAdapter snapshotAnim) { cancelAnimation(t, true /* restarting */, true /* forwardCancel */); mAnimation = anim; mAnimationType = type; @@ -177,7 +175,6 @@ public class SurfaceAnimator { cancelAnimation(); return; } - mLeash = freezer != null ? freezer.takeLeashForAnimation() : null; if (mLeash == null) { mLeash = createAnimationLeash(mAnimatable, surface, t, type, mAnimatable.getSurfaceWidth(), mAnimatable.getSurfaceHeight(), 0 /* x */, @@ -192,21 +189,13 @@ public class SurfaceAnimator { mAnimation.dump(pw, ""); ProtoLog.d(WM_DEBUG_ANIM, "Animation start for %s, anim=%s", mAnimatable, sw); } - if (snapshotAnim != null) { - mSnapshot = freezer.takeSnapshotForAnimation(); - if (mSnapshot == null) { - Slog.e(TAG, "No snapshot target to start animation on for " + mAnimatable); - return; - } - mSnapshot.startAnimation(t, snapshotAnim, type); - } setAnimatorPendingState(t); } void startAnimation(Transaction t, AnimationAdapter anim, boolean hidden, @AnimationType int type) { startAnimation(t, anim, hidden, type, null /* animationFinishedCallback */, - null /* animationCancelledCallback */, null /* snapshotAnim */, null /* freezer */); + null /* animationCancelledCallback */, null /* snapshotAnim */); } /** Indicates that there are surface operations in the pending transaction. */ @@ -317,7 +306,6 @@ public class SurfaceAnimator { final OnAnimationFinishedCallback animationFinishedCallback = mSurfaceAnimationFinishedCallback; final Runnable animationCancelledCallback = mAnimationCancelledCallback; - final SurfaceFreezer.Snapshot snapshot = mSnapshot; reset(t, false); if (animation != null) { if (forwardCancel) { @@ -337,9 +325,6 @@ public class SurfaceAnimator { } if (forwardCancel) { - if (snapshot != null) { - snapshot.cancelAnimation(t, false /* restarting */); - } if (leash != null) { t.remove(leash); mService.scheduleAnimationLocked(); @@ -352,12 +337,6 @@ public class SurfaceAnimator { mAnimation = null; mSurfaceAnimationFinishedCallback = null; mAnimationType = ANIMATION_TYPE_NONE; - final SurfaceFreezer.Snapshot snapshot = mSnapshot; - mSnapshot = null; - if (snapshot != null) { - // Reset the mSnapshot reference before calling the callback to prevent circular reset. - snapshot.cancelAnimation(t, !destroyLeash); - } if (mLeash == null) { return; } @@ -597,8 +576,7 @@ public class SurfaceAnimator { void commitPendingTransaction(); /** - * Called when the animation leash is created. Note that this is also called by - * {@link SurfaceFreezer}, so this doesn't mean we're about to start animating. + * Called when the animation leash is created. * * @param t The transaction to use to apply any necessary changes. * @param leash The leash that was created. diff --git a/services/core/java/com/android/server/wm/SurfaceFreezer.java b/services/core/java/com/android/server/wm/SurfaceFreezer.java deleted file mode 100644 index e126ed65d508..000000000000 --- a/services/core/java/com/android/server/wm/SurfaceFreezer.java +++ /dev/null @@ -1,303 +0,0 @@ -/* - * 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.server.wm; - -import static com.android.internal.protolog.WmProtoLogGroups.WM_SHOW_TRANSACTIONS; -import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_SCREEN_ROTATION; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.graphics.GraphicBuffer; -import android.graphics.PixelFormat; -import android.graphics.Point; -import android.graphics.Rect; -import android.hardware.HardwareBuffer; -import android.util.Slog; -import android.view.SurfaceControl; -import android.window.ScreenCapture; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.ProtoLog; - -/** - * This class handles "freezing" of an Animatable. The Animatable in question should implement - * Freezable. - * - * The point of this is to enable WindowContainers to each be capable of freezing themselves. - * Freezing means taking a snapshot and placing it above everything in the sub-hierarchy. - * The "placing above" requires that a parent surface be inserted above the target surface so that - * the target surface and the snapshot are siblings. - * - * The overall flow for a transition using this would be: - * 1. Set transition and record animatable in mChangingApps - * 2. Call {@link #freeze} to set-up the leashes and cover with a snapshot. - * 3. When transition participants are ready, start SurfaceAnimator with this as a parameter - * 4. SurfaceAnimator will then {@link #takeLeashForAnimation} instead of creating another leash. - * 5. The animation system should eventually clean this up via {@link #unfreeze}. - */ -class SurfaceFreezer { - - private static final String TAG = "SurfaceFreezer"; - - private final @NonNull Freezable mAnimatable; - private final @NonNull WindowManagerService mWmService; - @VisibleForTesting - SurfaceControl mLeash; - Snapshot mSnapshot = null; - final Rect mFreezeBounds = new Rect(); - - /** - * @param animatable The object to animate. - */ - SurfaceFreezer(@NonNull Freezable animatable, @NonNull WindowManagerService service) { - mAnimatable = animatable; - mWmService = service; - } - - /** - * Freeze the target surface. This is done by creating a leash (inserting a parent surface - * above the target surface) and then taking a snapshot and placing it over the target surface. - * - * @param startBounds The original bounds (on screen) of the surface we are snapshotting. - * @param relativePosition The related position of the snapshot surface to its parent. - * @param freezeTarget The surface to take snapshot from. If {@code null}, we will take a - * snapshot from the {@link #mAnimatable} surface. - */ - void freeze(SurfaceControl.Transaction t, Rect startBounds, Point relativePosition, - @Nullable SurfaceControl freezeTarget) { - reset(t); - mFreezeBounds.set(startBounds); - - mLeash = SurfaceAnimator.createAnimationLeash(mAnimatable, mAnimatable.getSurfaceControl(), - t, ANIMATION_TYPE_SCREEN_ROTATION, startBounds.width(), startBounds.height(), - relativePosition.x, relativePosition.y, false /* hidden */, - mWmService.mTransactionFactory); - mAnimatable.onAnimationLeashCreated(t, mLeash); - - freezeTarget = freezeTarget != null ? freezeTarget : mAnimatable.getFreezeSnapshotTarget(); - if (freezeTarget != null) { - ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer = createSnapshotBufferInner( - freezeTarget, startBounds); - final HardwareBuffer buffer = screenshotBuffer == null ? null - : screenshotBuffer.getHardwareBuffer(); - if (buffer == null || buffer.getWidth() <= 1 || buffer.getHeight() <= 1) { - // This can happen when display is not ready. - Slog.w(TAG, "Failed to capture screenshot for " + mAnimatable); - unfreeze(t); - return; - } - mSnapshot = new Snapshot(t, screenshotBuffer, mLeash); - } - } - - /** - * Used by {@link SurfaceAnimator}. This "transfers" the leash to be used for animation. - * By transferring the leash, this will no longer try to clean-up the leash when finished. - */ - SurfaceControl takeLeashForAnimation() { - SurfaceControl out = mLeash; - mLeash = null; - return out; - } - - /** - * Used by {@link SurfaceAnimator}. This "transfers" the snapshot leash to be used for - * animation. By transferring the leash, this will no longer try to clean-up the leash when - * finished. - */ - @Nullable - Snapshot takeSnapshotForAnimation() { - final Snapshot out = mSnapshot; - mSnapshot = null; - return out; - } - - /** - * Clean-up the snapshot and remove leash. If the leash was taken, this just cleans-up the - * snapshot. - */ - void unfreeze(SurfaceControl.Transaction t) { - unfreezeInner(t); - mAnimatable.onUnfrozen(); - } - - private void unfreezeInner(SurfaceControl.Transaction t) { - if (mSnapshot != null) { - mSnapshot.cancelAnimation(t, false /* restarting */); - mSnapshot = null; - } - if (mLeash == null) { - return; - } - SurfaceControl leash = mLeash; - mLeash = null; - final boolean scheduleAnim = SurfaceAnimator.removeLeash(t, mAnimatable, leash, - true /* destroy */); - if (scheduleAnim) { - mWmService.scheduleAnimationLocked(); - } - } - - /** Resets the snapshot before taking another one if the animation hasn't been started yet. */ - private void reset(SurfaceControl.Transaction t) { - // Those would have been taken by the SurfaceAnimator if the animation has been started, so - // we can remove the leash directly. - // No need to reset the mAnimatable leash, as this is called before a new animation leash is - // created, so another #onAnimationLeashCreated will be called. - if (mSnapshot != null) { - mSnapshot.destroy(t); - mSnapshot = null; - } - if (mLeash != null) { - t.remove(mLeash); - mLeash = null; - } - } - - void setLayer(SurfaceControl.Transaction t, int layer) { - if (mLeash != null) { - t.setLayer(mLeash, layer); - } - } - - void setRelativeLayer(SurfaceControl.Transaction t, SurfaceControl relativeTo, int layer) { - if (mLeash != null) { - t.setRelativeLayer(mLeash, relativeTo, layer); - } - } - - boolean hasLeash() { - return mLeash != null; - } - - private static ScreenCapture.ScreenshotHardwareBuffer createSnapshotBuffer( - @NonNull SurfaceControl target, @Nullable Rect bounds) { - Rect cropBounds = null; - if (bounds != null) { - cropBounds = new Rect(bounds); - cropBounds.offsetTo(0, 0); - } - ScreenCapture.LayerCaptureArgs captureArgs = - new ScreenCapture.LayerCaptureArgs.Builder(target) - .setSourceCrop(cropBounds) - .setCaptureSecureLayers(true) - .setAllowProtected(true) - .build(); - return ScreenCapture.captureLayers(captureArgs); - } - - @VisibleForTesting - ScreenCapture.ScreenshotHardwareBuffer createSnapshotBufferInner( - SurfaceControl target, Rect bounds) { - return createSnapshotBuffer(target, bounds); - } - - @VisibleForTesting - GraphicBuffer createFromHardwareBufferInner( - ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer) { - return GraphicBuffer.createFromHardwareBuffer(screenshotBuffer.getHardwareBuffer()); - } - - class Snapshot { - private SurfaceControl mSurfaceControl; - private AnimationAdapter mAnimation; - - /** - * @param t Transaction to create the thumbnail in. - * @param screenshotBuffer A thumbnail or placeholder for thumbnail to initialize with. - */ - Snapshot(SurfaceControl.Transaction t, - ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer, SurfaceControl parent) { - GraphicBuffer graphicBuffer = createFromHardwareBufferInner(screenshotBuffer); - - mSurfaceControl = mAnimatable.makeAnimationLeash() - .setName("snapshot anim: " + mAnimatable.toString()) - .setFormat(PixelFormat.TRANSLUCENT) - .setParent(parent) - .setSecure(screenshotBuffer.containsSecureLayers()) - .setCallsite("SurfaceFreezer.Snapshot") - .setBLASTLayer() - .build(); - - ProtoLog.i(WM_SHOW_TRANSACTIONS, " THUMBNAIL %s: CREATE", mSurfaceControl); - - t.setBuffer(mSurfaceControl, graphicBuffer); - t.setColorSpace(mSurfaceControl, screenshotBuffer.getColorSpace()); - t.show(mSurfaceControl); - - // We parent the thumbnail to the container, and just place it on top of anything else - // in the container. - t.setLayer(mSurfaceControl, Integer.MAX_VALUE); - } - - void destroy(SurfaceControl.Transaction t) { - if (mSurfaceControl == null) { - return; - } - t.remove(mSurfaceControl); - mSurfaceControl = null; - } - - /** - * Starts an animation. - * - * @param anim The object that bridges the controller, {@link SurfaceAnimator}, with the - * component responsible for running the animation. It runs the animation with - * {@link AnimationAdapter#startAnimation} once the hierarchy with - * the Leash has been set up. - */ - void startAnimation(SurfaceControl.Transaction t, AnimationAdapter anim, int type) { - cancelAnimation(t, true /* restarting */); - mAnimation = anim; - if (mSurfaceControl == null) { - cancelAnimation(t, false /* restarting */); - return; - } - mAnimation.startAnimation(mSurfaceControl, t, type, (typ, ani) -> { }); - } - - /** - * Cancels the animation, and resets the leash. - * - * @param t The transaction to use for all cancelling surface operations. - * @param restarting Whether we are restarting the animation. - */ - void cancelAnimation(SurfaceControl.Transaction t, boolean restarting) { - final SurfaceControl leash = mSurfaceControl; - final AnimationAdapter animation = mAnimation; - mAnimation = null; - if (animation != null) { - animation.onAnimationCancelled(leash); - } - if (!restarting) { - destroy(t); - } - } - } - - /** freezable */ - public interface Freezable extends SurfaceAnimator.Animatable { - /** - * @return The surface to take a snapshot of. If this returns {@code null}, no snapshot - * will be generated (but the rest of the freezing logic will still happen). - */ - @Nullable SurfaceControl getFreezeSnapshotTarget(); - - /** Called when the {@link #unfreeze(SurfaceControl.Transaction)} is called. */ - void onUnfrozen(); - } -} diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index c6136f316c3e..f2f926ac952c 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -52,11 +52,9 @@ import static android.view.SurfaceControl.METADATA_TASK_ID; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES; -import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_FLAG_APP_CRASHED; import static android.view.WindowManager.TRANSIT_NONE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; @@ -75,7 +73,6 @@ import static com.android.server.wm.ActivityRecord.TRANSFER_SPLASH_SCREEN_COPYIN import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RECENTS; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_SWITCH; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_TRANSITION; -import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_USER_LEAVING; import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_CLEANUP; import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_RECENTS; import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_SWITCH; @@ -161,13 +158,11 @@ import android.os.Trace; import android.os.UserHandle; import android.provider.Settings; import android.service.voice.IVoiceInteractionSession; -import android.util.ArraySet; import android.util.DisplayMetrics; import android.util.Slog; import android.util.proto.ProtoOutputStream; import android.view.DisplayInfo; import android.view.InsetsState; -import android.view.RemoteAnimationAdapter; import android.view.SurfaceControl; import android.view.WindowInsets; import android.view.WindowManager; @@ -2345,31 +2340,8 @@ class Task extends TaskFragment { } @VisibleForTesting - Point getLastSurfaceSize() { - return mLastSurfaceSize; - } - - @VisibleForTesting boolean isInChangeTransition() { - return mSurfaceFreezer.hasLeash() || AppTransition.isChangeTransitOld(mTransit); - } - - @Override - public SurfaceControl getFreezeSnapshotTarget() { - if (!mDisplayContent.mAppTransition.containsTransitRequest(TRANSIT_CHANGE)) { - return null; - } - // Skip creating snapshot if this transition is controlled by a remote animator which - // doesn't need it. - final ArraySet<Integer> activityTypes = new ArraySet<>(); - activityTypes.add(getActivityType()); - final RemoteAnimationAdapter adapter = - mDisplayContent.mAppTransitionController.getRemoteAnimationOverride( - this, TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE, activityTypes); - if (adapter != null && !adapter.getChangeNeedsSnapshot()) { - return null; - } - return getSurfaceControl(); + return AppTransition.isChangeTransitOld(mTransit); } @Override diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index fc7437b95e03..ba48fdc963de 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -1195,10 +1195,8 @@ class TaskFragment extends WindowContainer<WindowContainer> { if (!isAttached() || isForceHidden() || isForceTranslucent()) { return true; } - // A TaskFragment isn't translucent if it has at least one visible activity that occludes - // this TaskFragment. - return mTaskSupervisor.mOpaqueActivityHelper.getVisibleOpaqueActivity(this, - starting, true /* ignoringKeyguard */) == null; + return !mTaskSupervisor.mOpaqueContainerHelper.isOpaque( + this, starting, true /* ignoringKeyguard */, true /* ignoringInvisibleActivity */); } /** @@ -1211,7 +1209,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { return true; } // Including finishing Activity if the TaskFragment is becoming invisible in the transition. - return mTaskSupervisor.mOpaqueActivityHelper.getOpaqueActivity(this) == null; + return !mTaskSupervisor.mOpaqueContainerHelper.isOpaque(this); } /** @@ -1222,8 +1220,8 @@ class TaskFragment extends WindowContainer<WindowContainer> { if (!isAttached() || isForceHidden() || isForceTranslucent()) { return true; } - return mTaskSupervisor.mOpaqueActivityHelper.getVisibleOpaqueActivity(this, null, - false /* ignoringKeyguard */) == null; + return !mTaskSupervisor.mOpaqueContainerHelper.isOpaque(this, /* starting */ null, + false /* ignoringKeyguard */, true /* ignoringInvisibleActivity */); } ActivityRecord getTopNonFinishingActivity() { @@ -2758,7 +2756,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { // We only want to update for organized TaskFragment. Task will handle itself. return; } - if (mSurfaceControl == null || mSurfaceAnimator.hasLeash() || mSurfaceFreezer.hasLeash()) { + if (mSurfaceControl == null || mSurfaceAnimator.hasLeash()) { return; } @@ -2900,20 +2898,6 @@ class TaskFragment extends WindowContainer<WindowContainer> { return task != null && !task.isDragResizing() && super.canStartChangeTransition(); } - /** - * Returns {@code true} if the starting bounds of the closing organized TaskFragment is - * recorded. Otherwise, return {@code false}. - */ - boolean setClosingChangingStartBoundsIfNeeded() { - if (isOrganizedTaskFragment() && mDisplayContent != null - && mDisplayContent.mChangingContainers.remove(this)) { - mDisplayContent.mClosingChangingContainers.put( - this, new Rect(mSurfaceFreezer.mFreezeBounds)); - return true; - } - return false; - } - @Override boolean isSyncFinished(BLASTSyncEngine.SyncGroup group) { return super.isSyncFinished(group) && isReadyToTransit(); diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 27683b2fcff2..5217a759c6ae 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -505,7 +505,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { final WindowContainer<?> sibling = rootParent.getChildAt(j); if (sibling == transientRoot) break; if (!sibling.getWindowConfiguration().isAlwaysOnTop() && mController.mAtm - .mTaskSupervisor.mOpaqueActivityHelper.getOpaqueActivity(sibling) != null) { + .mTaskSupervisor.mOpaqueContainerHelper.isOpaque(sibling)) { occludedCount++; break; } diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 883d8f95b612..225951dbd345 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -138,7 +138,7 @@ import java.util.function.Predicate; * changes are made to this class. */ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<E> - implements Comparable<WindowContainer>, Animatable, SurfaceFreezer.Freezable, + implements Comparable<WindowContainer>, Animatable, InsetsControlTarget { private static final String TAG = TAG_WITH_CLASS_NAME ? "WindowContainer" : TAG_WM; @@ -226,7 +226,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< @Nullable private SurfaceControl mAnimationLeash; - final SurfaceFreezer mSurfaceFreezer; protected final WindowManagerService mWmService; final TransitionController mTransitionController; @@ -361,7 +360,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< mTransitionController = mWmService.mAtmService.getTransitionController(); mSyncTransaction = wms.mTransactionFactory.get(); mSurfaceAnimator = new SurfaceAnimator(this, this::onAnimationFinished, wms); - mSurfaceFreezer = new SurfaceFreezer(this, wms); } /** @@ -908,7 +906,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< final DisplayContent dc = getDisplayContent(); if (dc != null) { dc.mClosingChangingContainers.remove(this); - mSurfaceFreezer.unfreeze(getSyncTransaction()); } while (!mChildren.isEmpty()) { final E child = mChildren.getLast(); @@ -1124,9 +1121,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< // Cancel any change transition queued-up for this container on the old display when // this container is moved from the old display. mDisplayContent.mClosingChangingContainers.remove(this); - if (mDisplayContent.mChangingContainers.remove(this)) { - mSurfaceFreezer.unfreeze(getSyncTransaction()); - } + mDisplayContent.mChangingContainers.remove(this); } mDisplayContent = dc; if (dc != null && dc != this && mPendingTransaction != null) { @@ -1434,33 +1429,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< return setVisibleRequested(newVisReq); } - /** - * Called when the visibility of a child is asked to change. This is before visibility actually - * changes (eg. a transition animation might play out first). - */ - void onChildVisibilityRequested(boolean visible) { - // If we are losing visibility, then a snapshot isn't necessary and we are no-longer - // part of a change transition. - if (!visible) { - boolean skipUnfreeze = false; - if (asTaskFragment() != null) { - // If the organized TaskFragment is closing while resizing, we want to keep track of - // its starting bounds to make sure the animation starts at the correct position. - // This should be called before unfreeze() because we record the starting bounds - // in SurfaceFreezer. - skipUnfreeze = asTaskFragment().setClosingChangingStartBoundsIfNeeded(); - } - - if (!skipUnfreeze) { - mSurfaceFreezer.unfreeze(getSyncTransaction()); - } - } - WindowContainer parent = getParent(); - if (parent != null) { - parent.onChildVisibilityRequested(visible); - } - } - /** Whether this window is closing while resizing. */ boolean isClosingWhenResizing() { return mDisplayContent != null @@ -1545,9 +1513,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< } void onAppTransitionDone() { - if (mSurfaceFreezer.hasLeash()) { - mSurfaceFreezer.unfreeze(getSyncTransaction()); - } for (int i = mChildren.size() - 1; i >= 0; --i) { final WindowContainer wc = mChildren.get(i); wc.onAppTransitionDone(); @@ -2773,15 +2738,9 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< } protected void setLayer(Transaction t, int layer) { - if (mSurfaceFreezer.hasLeash()) { - // When the freezer has created animation leash parent for the window, set the layer - // there instead. - mSurfaceFreezer.setLayer(t, layer); - } else { - // Route through surface animator to accommodate that our surface control might be - // attached to the leash, and leash is attached to parent container. - mSurfaceAnimator.setLayer(t, layer); - } + // Route through surface animator to accommodate that our surface control might be + // attached to the leash, and leash is attached to parent container. + mSurfaceAnimator.setLayer(t, layer); } int getLastLayer() { @@ -2793,20 +2752,14 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< } protected void setRelativeLayer(Transaction t, SurfaceControl relativeTo, int layer) { - if (mSurfaceFreezer.hasLeash()) { - // When the freezer has created animation leash parent for the window, set the layer - // there instead. - mSurfaceFreezer.setRelativeLayer(t, relativeTo, layer); - } else { - // Route through surface animator to accommodate that our surface control might be - // attached to the leash, and leash is attached to parent container. - mSurfaceAnimator.setRelativeLayer(t, relativeTo, layer); - } + // Route through surface animator to accommodate that our surface control might be + // attached to the leash, and leash is attached to parent container. + mSurfaceAnimator.setRelativeLayer(t, relativeTo, layer); } protected void reparentSurfaceControl(Transaction t, SurfaceControl newParent) { // Don't reparent active leashes since the animator won't know about the change. - if (mSurfaceFreezer.hasLeash() || mSurfaceAnimator.hasLeash()) return; + if (mSurfaceAnimator.hasLeash()) return; t.reparent(getSurfaceControl(), newParent); } @@ -3044,7 +2997,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< // TODO: This should use isVisible() but because isVisible has a really weird meaning at // the moment this doesn't work for all animatable window containers. mSurfaceAnimator.startAnimation(t, anim, hidden, type, animationFinishedCallback, - animationCancelledCallback, snapshotAnim, mSurfaceFreezer); + animationCancelledCallback, snapshotAnim); } void startAnimation(Transaction t, AnimationAdapter anim, boolean hidden, @@ -3066,7 +3019,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< void cancelAnimation() { doAnimationFinished(mSurfaceAnimator.getAnimationType(), mSurfaceAnimator.getAnimation()); mSurfaceAnimator.cancelAnimation(); - mSurfaceFreezer.unfreeze(getSyncTransaction()); } /** Whether we can start change transition with this window and current display status. */ @@ -3097,7 +3049,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< } /** - * Initializes a change transition. See {@link SurfaceFreezer} for more information. + * Initializes a change transition. * * For now, this will only be called for the following cases: * 1. {@link Task} is changing windowing mode between fullscreen and freeform. @@ -3109,8 +3061,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< * use case. * * @param startBounds The original bounds (on screen) of the surface we are snapshotting. - * @param freezeTarget The surface to take snapshot from. If {@code null}, we will take a - * snapshot from {@link #getFreezeSnapshotTarget()}. */ void initializeChangeTransition(Rect startBounds, @Nullable SurfaceControl freezeTarget) { if (mDisplayContent.mTransitionController.isShellTransitionsEnabled()) { @@ -3122,7 +3072,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< // Calculate the relative position in parent container. final Rect parentBounds = getParent().getBounds(); mTmpPoint.set(startBounds.left - parentBounds.left, startBounds.top - parentBounds.top); - mSurfaceFreezer.freeze(getSyncTransaction(), startBounds, mTmpPoint, freezeTarget); } void initializeChangeTransition(Rect startBounds) { @@ -3134,23 +3083,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< } @Override - public SurfaceControl getFreezeSnapshotTarget() { - // Only allow freezing if this window is in a TRANSIT_CHANGE - if (!mDisplayContent.mAppTransition.containsTransitRequest(TRANSIT_CHANGE) - || !mDisplayContent.mChangingContainers.contains(this)) { - return null; - } - return getSurfaceControl(); - } - - @Override - public void onUnfrozen() { - if (mDisplayContent != null) { - mDisplayContent.mChangingContainers.remove(this); - } - } - - @Override public Builder makeAnimationLeash() { return makeSurface().setContainerLayer(); } @@ -3279,7 +3211,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< this, mTmpPoint, localBounds, screenBounds, closingStartBounds, showBackdrop, false /* shouldCreateSnapshot */); } else { - final Rect startBounds = isChanging ? mSurfaceFreezer.mFreezeBounds : null; + final Rect startBounds = null; adapters = controller.createRemoteAnimationRecord( this, mTmpPoint, localBounds, screenBounds, startBounds, showBackdrop); } @@ -3298,16 +3230,12 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< mTmpRect.offsetTo(mTmpPoint.x, mTmpPoint.y); final AnimationAdapter adapter = new LocalAnimationAdapter( - new WindowChangeAnimationSpec(mSurfaceFreezer.mFreezeBounds, mTmpRect, + new WindowChangeAnimationSpec(null /* startBounds */, mTmpRect, displayInfo, durationScale, true /* isAppAnimation */, false /* isThumbnail */), getSurfaceAnimationRunner()); - final AnimationAdapter thumbnailAdapter = mSurfaceFreezer.mSnapshot != null - ? new LocalAnimationAdapter(new WindowChangeAnimationSpec( - mSurfaceFreezer.mFreezeBounds, mTmpRect, displayInfo, durationScale, - true /* isAppAnimation */, true /* isThumbnail */), getSurfaceAnimationRunner()) - : null; + final AnimationAdapter thumbnailAdapter = null; resultAdapters = new Pair<>(adapter, thumbnailAdapter); mTransit = transit; mTransitFlags = getDisplayContent().mAppTransition.getTransitFlags(); @@ -3731,7 +3659,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED) void updateSurfacePosition(Transaction t) { - if (mSurfaceControl == null || mSurfaceAnimator.hasLeash() || mSurfaceFreezer.hasLeash()) { + if (mSurfaceControl == null || mSurfaceAnimator.hasLeash()) { return; } diff --git a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt index c62cd6e962b3..7128af5464e8 100644 --- a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt +++ b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt @@ -467,11 +467,17 @@ class PermissionService(private val service: AccessCheckingService) : override fun getPermissionRequestState( packageName: String, permissionName: String, - deviceId: String + deviceId: Int, + persistentDeviceId: String ): Int { val pid = Binder.getCallingPid() val uid = Binder.getCallingUid() - val result = context.checkPermission(permissionName, pid, uid) + val deviceContext = if (deviceId == context.deviceId){ + context + } else { + context.createDeviceContext(deviceId) + } + val result = deviceContext.checkPermission(permissionName, pid, uid) if (result == PackageManager.PERMISSION_GRANTED) { return Context.PERMISSION_REQUEST_STATE_GRANTED } @@ -497,14 +503,14 @@ class PermissionService(private val service: AccessCheckingService) : val permissionFlags = service.getState { - getPermissionFlagsWithPolicy(appId, userId, permissionName, deviceId) + getPermissionFlagsWithPolicy(appId, userId, permissionName, persistentDeviceId) } val isUnreqestable = permissionFlags.hasAnyBit(UNREQUESTABLE_MASK) // Special case for READ_MEDIA_IMAGES due to photo picker if ((permissionName == Manifest.permission.READ_MEDIA_IMAGES || permissionName == Manifest.permission.READ_MEDIA_VIDEO) && isUnreqestable) { val isUserSelectedGranted = - context.checkPermission( + deviceContext.checkPermission( Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED, pid, uid, @@ -515,7 +521,7 @@ class PermissionService(private val service: AccessCheckingService) : appId, userId, Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED, - deviceId, + persistentDeviceId, ) } if ( 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 a0e18ff15770..e0e44252a8f3 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -107,6 +107,7 @@ import android.hardware.display.DisplayManagerGlobal; import android.hardware.display.DisplayManagerInternal; import android.hardware.display.DisplayManagerInternal.DisplayOffloader; import android.hardware.display.DisplayTopology; +import android.hardware.display.DisplayTopologyGraph; import android.hardware.display.DisplayViewport; import android.hardware.display.DisplayedContentSample; import android.hardware.display.DisplayedContentSamplingAttributes; @@ -136,6 +137,7 @@ import android.provider.Settings.SettingNotFoundException; import android.test.mock.MockContentResolver; import android.util.Slog; import android.util.SparseArray; +import android.util.SparseIntArray; import android.util.Spline; import android.view.ContentRecordingSession; import android.view.Display; @@ -3875,8 +3877,16 @@ public class DisplayManagerServiceTest { displayManager.new BinderService(); registerDefaultDisplays(displayManager); initDisplayPowerController(localService); + displayManager.windowManagerAndInputReady(); + + DisplayTopology topology = mock(DisplayTopology.class); + when(topology.copy()).thenReturn(topology); + DisplayTopologyGraph graph = mock(DisplayTopologyGraph.class); + when(topology.getGraph(any(SparseIntArray.class))).thenReturn(graph); + displayManagerBinderService.setDisplayTopology(topology); + waitForIdleHandler(displayManager.getDisplayHandler()); - displayManagerBinderService.setDisplayTopology(new DisplayTopology()); + verify(mMockInputManagerInternal).setDisplayTopology(graph); } @Test diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt b/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt index 04dff1d36495..ca670488f6e3 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt @@ -18,39 +18,51 @@ package com.android.server.display import android.hardware.display.DisplayTopology import android.hardware.display.DisplayTopology.pxToDp +import android.hardware.display.DisplayTopologyGraph import android.util.SparseArray +import android.util.SparseIntArray import android.view.Display import android.view.DisplayInfo import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test +import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.anyFloat import org.mockito.ArgumentMatchers.anyInt import org.mockito.kotlin.any +import org.mockito.kotlin.clearInvocations import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never +import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever class DisplayTopologyCoordinatorTest { private lateinit var coordinator: DisplayTopologyCoordinator - private val displayInfo = DisplayInfo() + private lateinit var displayInfos: List<DisplayInfo> private val topologyChangeExecutor = Runnable::run private val mockTopologyStore = mock<DisplayTopologyStore>() private val mockTopology = mock<DisplayTopology>() private val mockTopologyCopy = mock<DisplayTopology>() + private val mockTopologyGraph = mock<DisplayTopologyGraph>() private val mockIsExtendedDisplayEnabled = mock<() -> Boolean>() private val mockTopologySavedCallback = mock<() -> Unit>() - private val mockTopologyChangedCallback = mock<(DisplayTopology) -> Unit>() + private val mockTopologyChangedCallback = + mock<(android.util.Pair<DisplayTopology, DisplayTopologyGraph>) -> Unit>() @Before fun setUp() { - displayInfo.displayId = Display.DEFAULT_DISPLAY - displayInfo.logicalWidth = 300 - displayInfo.logicalHeight = 200 - displayInfo.logicalDensityDpi = 100 + displayInfos = (1..10).map { i -> + val info = DisplayInfo() + info.displayId = i + info.displayGroupId = Display.DEFAULT_DISPLAY_GROUP + info.logicalWidth = i * 300 + info.logicalHeight = i * 200 + info.logicalDensityDpi = i * 100 + return@map info + } val injector = object : DisplayTopologyCoordinator.Injector() { override fun getTopology() = mockTopology @@ -62,6 +74,7 @@ class DisplayTopologyCoordinatorTest { } whenever(mockIsExtendedDisplayEnabled()).thenReturn(true) whenever(mockTopology.copy()).thenReturn(mockTopologyCopy) + whenever(mockTopologyCopy.getGraph(any())).thenReturn(mockTopologyGraph) coordinator = DisplayTopologyCoordinator(injector, mockIsExtendedDisplayEnabled, mockTopologyChangedCallback, topologyChangeExecutor, DisplayManagerService.SyncRoot(), mockTopologySavedCallback) @@ -69,20 +82,45 @@ class DisplayTopologyCoordinatorTest { @Test fun addDisplay() { - coordinator.onDisplayAdded(displayInfo) + displayInfos.forEachIndexed { i, displayInfo -> + coordinator.onDisplayAdded(displayInfo) + + val widthDp = pxToDp(displayInfo.logicalWidth.toFloat(), displayInfo.logicalDensityDpi) + val heightDp = + pxToDp(displayInfo.logicalHeight.toFloat(), displayInfo.logicalDensityDpi) + verify(mockTopology).addDisplay(displayInfo.displayId, widthDp, heightDp) + } + + val captor = ArgumentCaptor.forClass(SparseIntArray::class.java) + verify(mockTopologyCopy, times(displayInfos.size)).getGraph(captor.capture()) + val densities = captor.value + assertThat(densities.size()).isEqualTo(displayInfos.size) + for (displayInfo in displayInfos) { + assertThat(densities.get(displayInfo.displayId)) + .isEqualTo(displayInfo.logicalDensityDpi) + } + + verify(mockTopologyChangedCallback, times(displayInfos.size)).invoke( + android.util.Pair( + mockTopologyCopy, + mockTopologyGraph + ) + ) + verify(mockTopologyStore, times(displayInfos.size)).restoreTopology(mockTopology) - val widthDp = pxToDp(displayInfo.logicalWidth.toFloat(), displayInfo.logicalDensityDpi) - val heightDp = pxToDp(displayInfo.logicalHeight.toFloat(), displayInfo.logicalDensityDpi) - verify(mockTopology).addDisplay(displayInfo.displayId, widthDp, heightDp) - verify(mockTopologyChangedCallback).invoke(mockTopologyCopy) - verify(mockTopologyStore).restoreTopology(mockTopology) + // Clear invocations for other tests that call this method + clearInvocations(mockTopologyCopy) + clearInvocations(mockTopologyChangedCallback) + clearInvocations(mockTopologyStore) } @Test fun addDisplay_extendedDisplaysDisabled() { whenever(mockIsExtendedDisplayEnabled()).thenReturn(false) - coordinator.onDisplayAdded(displayInfo) + for (displayInfo in displayInfos) { + coordinator.onDisplayAdded(displayInfo) + } verify(mockTopology, never()).addDisplay(anyInt(), anyFloat(), anyFloat()) verify(mockTopologyChangedCallback, never()).invoke(any()) @@ -91,9 +129,9 @@ class DisplayTopologyCoordinatorTest { @Test fun addDisplay_notInDefaultDisplayGroup() { - displayInfo.displayGroupId = Display.DEFAULT_DISPLAY_GROUP + 1 + displayInfos[0].displayGroupId = Display.DEFAULT_DISPLAY_GROUP + 1 - coordinator.onDisplayAdded(displayInfo) + coordinator.onDisplayAdded(displayInfos[0]) verify(mockTopology, never()).addDisplay(anyInt(), anyFloat(), anyFloat()) verify(mockTopologyChangedCallback, never()).invoke(any()) @@ -102,39 +140,102 @@ class DisplayTopologyCoordinatorTest { @Test fun updateDisplay() { - whenever(mockTopology.updateDisplay(eq(Display.DEFAULT_DISPLAY), anyFloat(), anyFloat())) + whenever(mockTopology.updateDisplay(eq(displayInfos[0].displayId), anyFloat(), anyFloat())) .thenReturn(true) + addDisplay() + + displayInfos[0].logicalDensityDpi += 10 + coordinator.onDisplayChanged(displayInfos[0]) - coordinator.onDisplayChanged(displayInfo) + val captor = ArgumentCaptor.forClass(SparseIntArray::class.java) + verify(mockTopologyCopy).getGraph(captor.capture()) + val densities = captor.value + assertThat(densities.size()).isEqualTo(displayInfos.size) + assertThat(densities.get(displayInfos[0].displayId)) + .isEqualTo(displayInfos[0].logicalDensityDpi) - verify(mockTopologyChangedCallback).invoke(mockTopologyCopy) + verify(mockTopologyChangedCallback).invoke( + android.util.Pair( + mockTopologyCopy, + mockTopologyGraph + ) + ) } @Test fun updateDisplay_notChanged() { - whenever(mockTopology.updateDisplay(eq(Display.DEFAULT_DISPLAY), anyFloat(), anyFloat())) - .thenReturn(false) + addDisplay() - coordinator.onDisplayChanged(displayInfo) + for (displayInfo in displayInfos) { + coordinator.onDisplayChanged(displayInfo) + } + + // Try to update a display that does not exist + val info = DisplayInfo() + info.displayId = 100 + coordinator.onDisplayChanged(info) + verify(mockTopologyCopy, never()).getGraph(any()) + verify(mockTopologyChangedCallback, never()).invoke(any()) + } + + @Test + fun updateDisplay_extendedDisplaysDisabled() { + whenever(mockIsExtendedDisplayEnabled()).thenReturn(false) + + for (displayInfo in displayInfos) { + coordinator.onDisplayAdded(displayInfo) + } + + verify(mockTopology, never()).updateDisplay(anyInt(), anyFloat(), anyFloat()) + verify(mockTopologyCopy, never()).getGraph(any()) + verify(mockTopologyChangedCallback, never()).invoke(any()) + } + + @Test + fun updateDisplay_notInDefaultDisplayGroup() { + displayInfos[0].displayGroupId = Display.DEFAULT_DISPLAY_GROUP + 1 + + coordinator.onDisplayAdded(displayInfos[0]) + + verify(mockTopology, never()).updateDisplay(anyInt(), anyFloat(), anyFloat()) + verify(mockTopologyCopy, never()).getGraph(any()) verify(mockTopologyChangedCallback, never()).invoke(any()) } @Test fun removeDisplay() { - whenever(mockTopology.removeDisplay(Display.DEFAULT_DISPLAY)).thenReturn(true) + addDisplay() - coordinator.onDisplayRemoved(Display.DEFAULT_DISPLAY) + val displaysToRemove = listOf(0, 2, 3).map { displayInfos[it] } + for (displayInfo in displaysToRemove) { + whenever(mockTopology.removeDisplay(displayInfo.displayId)).thenReturn(true) - verify(mockTopologyChangedCallback).invoke(mockTopologyCopy) - verify(mockTopologyStore).restoreTopology(mockTopology) + coordinator.onDisplayRemoved(displayInfo.displayId) + } + + val captor = ArgumentCaptor.forClass(SparseIntArray::class.java) + verify(mockTopologyCopy, times(displaysToRemove.size)).getGraph(captor.capture()) + val densities = captor.value + assertThat(densities.size()).isEqualTo(displayInfos.size - displaysToRemove.size) + for (displayInfo in displaysToRemove) { + assertThat(densities.get(displayInfo.displayId)).isEqualTo(0) + } + + verify(mockTopologyChangedCallback, times(displaysToRemove.size)).invoke( + android.util.Pair( + mockTopologyCopy, + mockTopologyGraph + ) + ) + verify(mockTopologyStore, times(displaysToRemove.size)).restoreTopology(mockTopology) } @Test fun removeDisplay_notChanged() { - whenever(mockTopology.removeDisplay(Display.DEFAULT_DISPLAY)).thenReturn(false) - - coordinator.onDisplayRemoved(Display.DEFAULT_DISPLAY) + for (displayInfo in displayInfos) { + coordinator.onDisplayRemoved(displayInfo.displayId) + } verify(mockTopologyChangedCallback, never()).invoke(any()) verify(mockTopologyStore, never()).restoreTopology(any()) @@ -149,12 +250,20 @@ class DisplayTopologyCoordinatorTest { fun setTopology_normalize() { val topology = mock<DisplayTopology>() val topologyCopy = mock<DisplayTopology>() + val topologyGraph = mock<DisplayTopologyGraph>() whenever(topology.copy()).thenReturn(topologyCopy) + whenever(topologyCopy.getGraph(any())).thenReturn(topologyGraph) whenever(mockTopologyStore.saveTopology(topology)).thenReturn(true) + coordinator.topology = topology verify(topology).normalize() - verify(mockTopologyChangedCallback).invoke(topologyCopy) + verify(mockTopologyChangedCallback).invoke( + android.util.Pair( + topologyCopy, + topologyGraph + ) + ) verify(mockTopologyStore).saveTopology(topology) verify(mockTopologySavedCallback).invoke() } diff --git a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java index 6b138b986fe7..29a17e1c85ab 100644 --- a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java +++ b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java @@ -89,9 +89,13 @@ import android.os.PowerManager; import android.os.PowerManagerInternal; import android.os.PowerSaveState; import android.os.UserHandle; +import android.os.WorkSource; import android.os.test.TestLooper; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.DeviceConfig; import android.provider.Settings; @@ -200,6 +204,9 @@ public class PowerManagerServiceTest { @Rule public SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + private PowerManagerService mService; private ContextWrapper mContextSpy; private BatteryReceiver mBatteryReceiver; @@ -3045,6 +3052,40 @@ public class PowerManagerServiceTest { } /** + * Test IPowerManager.updateWakeLockUids() updates the workchain with the new uids + */ + @Test + @RequiresFlagsEnabled({Flags.FLAG_WAKELOCK_ATTRIBUTION_VIA_WORKCHAIN}) + public void test_updateWakelockUids_updatesWorkchain() { + createService(); + startSystem(); + final String tag = "wakelock1"; + final String packageName = "pkg.name"; + final IBinder token = new Binder(); + int flags = PowerManager.PARTIAL_WAKE_LOCK; + final IWakeLockCallback callback1 = Mockito.mock(IWakeLockCallback.class); + final IBinder callbackBinder1 = Mockito.mock(Binder.class); + when(callback1.asBinder()).thenReturn(callbackBinder1); + WorkSource oldWorksource = new WorkSource(); + oldWorksource.createWorkChain().addNode(1000, null); + mService.getBinderServiceInstance().acquireWakeLock(token, flags, tag, packageName, + oldWorksource, null /* historyTag */, Display.INVALID_DISPLAY, callback1); + verify(mNotifierMock).onWakeLockAcquired(anyInt(), eq(tag), eq(packageName), + anyInt(), anyInt(), eq(oldWorksource), any(), same(callback1)); + + WorkSource newWorksource = new WorkSource(); + newWorksource.createWorkChain().addNode(1011, null) + .addNode(Binder.getCallingUid(), null); + newWorksource.createWorkChain().addNode(1012, null) + .addNode(Binder.getCallingUid(), null); + mService.getBinderServiceInstance().updateWakeLockUids(token, new int[]{1011, 1012}); + verify(mNotifierMock).onWakeLockChanging(anyInt(), eq(tag), eq(packageName), + anyInt(), anyInt(), eq(oldWorksource), any(), any(), + anyInt(), eq(tag), eq(packageName), anyInt(), anyInt(), eq(newWorksource), any(), + any()); + } + + /** * Test IPowerManager.updateWakeLockCallback() with a new IWakeLockCallback. */ @Test diff --git a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java index 0bbae247d8bb..2d81f72e3319 100644 --- a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java @@ -117,6 +117,12 @@ public class VolumeHelperTest { /** Choose a default stream volume value which does not depend on min/max. */ private static final int DEFAULT_STREAM_VOLUME = 2; + /** + * The default ringer mode affected stream value since the ringer mode delegate is not used + * for unit testing. + */ + private static final int DEFAULT_RINGER_MODE_AFFECTED_STREAMS = 0x1a6; + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); @@ -186,6 +192,10 @@ public class VolumeHelperTest { public void setMuteAffectedStreams(int muteAffectedStreams) { mMuteAffectedStreams = muteAffectedStreams; } + + public void setRingerModeAffectedStreams(int ringerModeAffectedStreams) { + mRingerModeAffectedStreams = ringerModeAffectedStreams; + } } private static class TestDeviceVolumeBehaviorDispatcherStub @@ -550,6 +560,48 @@ public class VolumeHelperTest { assertEquals(RINGER_MODE_VIBRATE, mAudioService.getRingerModeInternal()); } + @Test + public void setStreamVolume_doesNotUnmuteStreamAffectedByRingerMode() throws Exception { + assumeFalse("Skipping ringer mode test on automotive", mIsAutomotive); + mAudioService.setRingerModeAffectedStreams(DEFAULT_RINGER_MODE_AFFECTED_STREAMS); + mAudioService.setRingerModeInternal(RINGER_MODE_VIBRATE, mContext.getOpPackageName()); + + mAudioService.setStreamVolume(STREAM_NOTIFICATION, /*index=*/1, /*flags=*/0, + mContext.getOpPackageName()); + mTestLooper.dispatchAll(); + + assertEquals(0, mAudioService.getStreamVolume(STREAM_NOTIFICATION)); + } + + @Test + public void adjustUnmuteStreamVolume_doesNotUnmuteStreamAffectedByRingerMode() + throws Exception { + assumeFalse("Skipping ringer mode test on automotive", mIsAutomotive); + mAudioService.setRingerModeAffectedStreams(DEFAULT_RINGER_MODE_AFFECTED_STREAMS); + mAudioService.setRingerModeInternal(RINGER_MODE_VIBRATE, mContext.getOpPackageName()); + + mAudioService.adjustStreamVolume(STREAM_NOTIFICATION, ADJUST_UNMUTE, /*flags=*/0, + mContext.getOpPackageName()); + mTestLooper.dispatchAll(); + + assertEquals(0, mAudioService.getStreamVolume(STREAM_NOTIFICATION)); + } + + @Test + public void adjustRaiseStreamVolume_doesNotUnmuteStreamAffectedByRingerMode() + throws Exception { + assumeFalse("Skipping ringer mode test on automotive", mIsAutomotive); + mAudioService.setRingerModeAffectedStreams(DEFAULT_RINGER_MODE_AFFECTED_STREAMS); + mAudioService.setRingerModeInternal(RINGER_MODE_VIBRATE, mContext.getOpPackageName()); + + mAudioService.adjustStreamVolume(STREAM_NOTIFICATION, ADJUST_RAISE, /*flags=*/0, + mContext.getOpPackageName()); + mTestLooper.dispatchAll(); + + assertEquals(0, mAudioService.getStreamVolume(STREAM_NOTIFICATION)); + } + + // --------------------- Permission tests --------------------- @Test diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java index 6d863015231c..fcde4055cf17 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java @@ -16,12 +16,15 @@ package com.android.server.hdmi; +import static android.content.pm.PackageManager.FEATURE_HDMI_CEC; + import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; import static com.android.server.hdmi.Constants.ADDR_TV; import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; import static org.mockito.Mockito.spy; import android.annotation.RequiresPermission; @@ -60,6 +63,9 @@ public class ActiveSourceActionTest { @Before public void setUp() throws Exception { + assumeTrue("Test requires FEATURE_HDMI_CEC", + InstrumentationRegistry.getTargetContext().getPackageManager() + .hasSystemFeature(FEATURE_HDMI_CEC)); mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); FakeAudioFramework audioFramework = new FakeAudioFramework(); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java index a5f7bb117e7d..9a6a24c52143 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java @@ -15,11 +15,14 @@ */ package com.android.server.hdmi; +import static android.content.pm.PackageManager.FEATURE_HDMI_CEC; + import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; import static org.mockito.Mockito.spy; import android.annotation.RequiresPermission; @@ -59,6 +62,9 @@ public class ArcInitiationActionFromAvrTest { @Before public void setUp() throws Exception { + assumeTrue("Test requires FEATURE_HDMI_CEC", + InstrumentationRegistry.getTargetContext().getPackageManager() + .hasSystemFeature(FEATURE_HDMI_CEC)); mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); FakeAudioFramework audioFramework = new FakeAudioFramework(); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java index 857ee1aa176f..ee2f1767d849 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java @@ -15,11 +15,14 @@ */ package com.android.server.hdmi; +import static android.content.pm.PackageManager.FEATURE_HDMI_CEC; + import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; import static org.mockito.Mockito.spy; import android.annotation.RequiresPermission; @@ -64,6 +67,9 @@ public class ArcTerminationActionFromAvrTest { @Before public void setUp() throws Exception { + assumeTrue("Test requires FEATURE_HDMI_CEC", + InstrumentationRegistry.getTargetContext().getPackageManager() + .hasSystemFeature(FEATURE_HDMI_CEC)); MockitoAnnotations.initMocks(this); mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java index 2296911a4e7e..40f9dbc074c5 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java @@ -16,12 +16,15 @@ package com.android.server.hdmi; +import static android.content.pm.PackageManager.FEATURE_HDMI_CEC; + import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; import static com.android.server.hdmi.Constants.ADDR_BROADCAST; import static com.android.server.hdmi.Constants.ADDR_TV; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -73,6 +76,9 @@ public class DevicePowerStatusActionTest { @Before public void setUp() throws Exception { + assumeTrue("Test requires FEATURE_HDMI_CEC", + InstrumentationRegistry.getTargetContext().getPackageManager() + .hasSystemFeature(FEATURE_HDMI_CEC)); MockitoAnnotations.initMocks(this); mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java index 47cfa4218435..461e98b4bf06 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java @@ -16,6 +16,7 @@ package com.android.server.hdmi; +import static android.content.pm.PackageManager.FEATURE_HDMI_CEC; import static android.hardware.hdmi.HdmiControlManager.POWER_STATUS_ON; import static android.hardware.hdmi.HdmiControlManager.POWER_STATUS_STANDBY; import static android.hardware.hdmi.HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON; @@ -30,6 +31,8 @@ import static com.android.server.hdmi.DeviceSelectActionFromPlayback.STATE_WAIT_ import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; + import android.annotation.RequiresPermission; import android.content.Context; import android.content.Intent; @@ -94,6 +97,9 @@ public class DeviceSelectActionFromPlaybackTest { @Before public void setUp() { + assumeTrue("Test requires FEATURE_HDMI_CEC", + InstrumentationRegistry.getTargetContext().getPackageManager() + .hasSystemFeature(FEATURE_HDMI_CEC)); MockitoAnnotations.initMocks(this); Context context = InstrumentationRegistry.getTargetContext(); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java index 792faab5b196..a4c71bd6094e 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java @@ -16,6 +16,7 @@ package com.android.server.hdmi; +import static android.content.pm.PackageManager.FEATURE_HDMI_CEC; import static android.hardware.hdmi.HdmiControlManager.POWER_STATUS_ON; import static android.hardware.hdmi.HdmiControlManager.POWER_STATUS_STANDBY; import static android.hardware.hdmi.HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON; @@ -30,6 +31,8 @@ import static com.android.server.hdmi.DeviceSelectActionFromTv.STATE_WAIT_FOR_RE import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; + import android.annotation.RequiresPermission; import android.content.Context; import android.content.Intent; @@ -106,6 +109,9 @@ public class DeviceSelectActionFromTvTest { @Before public void setUp() { + assumeTrue("Test requires FEATURE_HDMI_CEC", + InstrumentationRegistry.getTargetContext().getPackageManager() + .hasSystemFeature(FEATURE_HDMI_CEC)); Context context = InstrumentationRegistry.getTargetContext(); mMyLooper = mTestLooper.getLooper(); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java index 30dac9f3813d..0e9f21948907 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java @@ -15,12 +15,15 @@ */ package com.android.server.hdmi; +import static android.content.pm.PackageManager.FEATURE_HDMI_CEC; + import static com.android.server.SystemService.PHASE_BOOT_COMPLETED; import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1; import static com.android.server.hdmi.Constants.ADDR_TV; import static com.android.server.hdmi.Constants.PATH_RELATIONSHIP_ANCESTOR; import static com.android.server.hdmi.HdmiControlService.WAKE_UP_SCREEN_ON; +import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -92,6 +95,9 @@ public class HdmiCecAtomLoggingTest { @Before public void setUp() throws RemoteException { + assumeTrue("Test requires FEATURE_HDMI_CEC", + InstrumentationRegistry.getTargetContext().getPackageManager() + .hasSystemFeature(FEATURE_HDMI_CEC)); mHdmiCecAtomWriterSpy = spy(new HdmiCecAtomWriter()); mLooper = mTestLooper.getLooper(); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTvTest.java index e1e101fc1724..a0e21ed1bdb1 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTvTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTvTest.java @@ -15,10 +15,13 @@ */ package com.android.server.hdmi; +import static android.content.pm.PackageManager.FEATURE_HDMI_CEC; + import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_EARC_PENDING; import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_UNKNOWN; import static com.android.server.hdmi.HdmiControlService.WAKE_UP_SCREEN_ON; +import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; @@ -78,6 +81,9 @@ public class HdmiCecAtomLoggingTvTest { @Before public void setUp() throws RemoteException { + assumeTrue("Test requires FEATURE_HDMI_CEC", + InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageManager() + .hasSystemFeature(FEATURE_HDMI_CEC)); mHdmiCecAtomWriterSpy = spy(new HdmiCecAtomWriter()); mLooper = mTestLooper.getLooper(); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecConfigTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecConfigTest.java index d8c58a8e16b6..e66026735ec4 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecConfigTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecConfigTest.java @@ -15,10 +15,13 @@ */ package com.android.server.hdmi; +import static android.content.pm.PackageManager.FEATURE_HDMI_CEC; + import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -65,6 +68,9 @@ public final class HdmiCecConfigTest { @Before public void setUp() throws Exception { + assumeTrue("Test requires FEATURE_HDMI_CEC", + InstrumentationRegistry.getTargetContext().getPackageManager() + .hasSystemFeature(FEATURE_HDMI_CEC)); MockitoAnnotations.initMocks(this); mContext = FakeHdmiCecConfig.buildContext(InstrumentationRegistry.getTargetContext()); } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java index 4f551119b42e..314fe05b6367 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java @@ -15,6 +15,8 @@ */ package com.android.server.hdmi; +import static android.content.pm.PackageManager.FEATURE_HDMI_CEC; + import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM; import static com.android.server.hdmi.Constants.ADDR_BROADCAST; @@ -26,6 +28,8 @@ import static com.android.server.hdmi.HdmiControlService.STANDBY_SCREEN_OFF; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; + import android.annotation.RequiresPermission; import android.content.Context; import android.content.Intent; @@ -85,6 +89,9 @@ public class HdmiCecLocalDeviceAudioSystemTest { @Before public void setUp() { + assumeTrue("Test requires FEATURE_HDMI_CEC", + InstrumentationRegistry.getTargetContext().getPackageManager() + .hasSystemFeature(FEATURE_HDMI_CEC)); Context context = InstrumentationRegistry.getTargetContext(); mMyLooper = mTestLooper.getLooper(); mLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_PLAYBACK); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java index cfdf17668229..d600e16c6f13 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java @@ -15,6 +15,8 @@ */ package com.android.server.hdmi; +import static android.content.pm.PackageManager.FEATURE_HDMI_CEC; + import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; import static com.android.server.hdmi.Constants.ABORT_UNRECOGNIZED_OPCODE; import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM; @@ -29,6 +31,8 @@ import static com.android.server.hdmi.PowerStatusMonitorActionFromPlayback.MONIT import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; + import android.annotation.RequiresPermission; import android.content.Context; import android.content.Intent; @@ -97,6 +101,9 @@ public class HdmiCecLocalDevicePlaybackTest { new FakePowerManagerInternalWrapper(); @Before public void setUp() { + assumeTrue("Test requires FEATURE_HDMI_CEC", + InstrumentationRegistry.getTargetContext().getPackageManager() + .hasSystemFeature(FEATURE_HDMI_CEC)); MockitoAnnotations.initMocks(this); Context context = InstrumentationRegistry.getTargetContext(); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java index 5be4490e67ef..f74e2ace7ae3 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java @@ -15,6 +15,8 @@ */ package com.android.server.hdmi; +import static android.content.pm.PackageManager.FEATURE_HDMI_CEC; + import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; import static com.android.server.hdmi.Constants.ABORT_UNRECOGNIZED_OPCODE; import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM; @@ -29,8 +31,8 @@ import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_WAKE_UP_ME import static com.android.server.hdmi.HdmiControlService.STANDBY_SCREEN_OFF; import static com.android.server.hdmi.HdmiControlService.WAKE_UP_SCREEN_ON; import static com.android.server.hdmi.RequestActiveSourceAction.TIMEOUT_WAIT_FOR_TV_ASSERT_ACTIVE_SOURCE_MS; -import static com.android.server.hdmi.RoutingControlAction.TIMEOUT_ROUTING_INFORMATION_MS; import static com.android.server.hdmi.RequestSadAction.RETRY_COUNTER_MAX; +import static com.android.server.hdmi.RoutingControlAction.TIMEOUT_ROUTING_INFORMATION_MS; import static com.google.common.truth.Truth.assertThat; @@ -38,6 +40,7 @@ import static junit.framework.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.eq; @@ -167,6 +170,9 @@ public class HdmiCecLocalDeviceTvTest { @Before public void setUp() { + assumeTrue("Test requires FEATURE_HDMI_CEC", + InstrumentationRegistry.getTargetContext().getPackageManager() + .hasSystemFeature(FEATURE_HDMI_CEC)); Context context = InstrumentationRegistry.getTargetContext(); mMyLooper = mTestLooper.getLooper(); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java index 6577e09a986e..587f4370636c 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java @@ -16,6 +16,8 @@ package com.android.server.hdmi; +import static android.content.pm.PackageManager.FEATURE_HDMI_CEC; + import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM; import static com.android.server.hdmi.Constants.ADDR_BROADCAST; import static com.android.server.hdmi.HdmiCecMessageValidator.ERROR_DESTINATION; @@ -27,6 +29,8 @@ import static com.android.server.hdmi.HdmiCecMessageValidator.OK; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; + import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; @@ -53,6 +57,9 @@ public class HdmiCecMessageValidatorTest { @Before public void setUp() throws Exception { + assumeTrue("Test requires FEATURE_HDMI_CEC", + InstrumentationRegistry.getTargetContext().getPackageManager() + .hasSystemFeature(FEATURE_HDMI_CEC)); FakeAudioFramework audioFramework = new FakeAudioFramework(); HdmiControlService mHdmiControlService = new HdmiControlService( diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java index 4f7f381a33d6..b1460b33cdf8 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java @@ -17,10 +17,13 @@ package com.android.server.hdmi; +import static android.content.pm.PackageManager.FEATURE_HDMI_CEC; import static android.hardware.hdmi.DeviceFeatures.FEATURE_SUPPORTED; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; + import android.content.Context; import android.hardware.hdmi.DeviceFeatures; import android.hardware.hdmi.HdmiControlManager; @@ -66,6 +69,9 @@ public class HdmiCecNetworkTest { @Before public void setUp() throws Exception { + assumeTrue("Test requires FEATURE_HDMI_CEC", + InstrumentationRegistry.getTargetContext().getPackageManager() + .hasSystemFeature(FEATURE_HDMI_CEC)); mContext = InstrumentationRegistry.getTargetContext(); FakeAudioFramework audioFramework = new FakeAudioFramework(); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java index 3361e7f359e2..c48e4b6cf710 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java @@ -15,10 +15,13 @@ */ package com.android.server.hdmi; +import static android.content.pm.PackageManager.FEATURE_HDMI_CEC; + import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; import static org.mockito.Mockito.spy; import android.annotation.RequiresPermission; @@ -63,6 +66,9 @@ public class HdmiCecPowerStatusControllerTest { @Before public void setUp() throws Exception { + assumeTrue("Test requires FEATURE_HDMI_CEC", + InstrumentationRegistry.getTargetContext().getPackageManager() + .hasSystemFeature(FEATURE_HDMI_CEC)); Context contextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); Looper myLooper = mTestLooper.getLooper(); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java index 126a65863f59..4eb3c15eed95 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java @@ -15,6 +15,7 @@ */ package com.android.server.hdmi; +import static android.content.pm.PackageManager.FEATURE_HDMI_CEC; import static android.hardware.hdmi.HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM; import static android.hardware.hdmi.HdmiDeviceInfo.DEVICE_PLAYBACK; import static android.hardware.hdmi.HdmiDeviceInfo.DEVICE_TV; @@ -38,6 +39,7 @@ import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; import static junit.framework.TestCase.assertEquals; +import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -111,6 +113,9 @@ public class HdmiControlServiceTest { @Before public void setUp() throws Exception { + assumeTrue("Test requires FEATURE_HDMI_CEC", + InstrumentationRegistry.getTargetContext().getPackageManager() + .hasSystemFeature(FEATURE_HDMI_CEC)); mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); HdmiCecConfig hdmiCecConfig = new FakeHdmiCecConfig(mContextSpy); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTvTest.java index eed99756abb1..d28306458d55 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTvTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTvTest.java @@ -16,12 +16,15 @@ package com.android.server.hdmi; +import static android.content.pm.PackageManager.FEATURE_HDMI_CEC; import static android.hardware.hdmi.HdmiDeviceInfo.DEVICE_TV; import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; + import android.content.Context; import android.hardware.hdmi.HdmiControlManager; import android.hardware.hdmi.HdmiDeviceInfo; @@ -60,6 +63,9 @@ public class HdmiControlServiceTvTest { @Before public void setUp() throws Exception { + assumeTrue("Test requires FEATURE_HDMI_CEC", + InstrumentationRegistry.getTargetContext().getPackageManager() + .hasSystemFeature(FEATURE_HDMI_CEC)); Context context = InstrumentationRegistry.getTargetContext(); mMyLooper = mTestLooper.getLooper(); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java index 185f90f4e803..98d2dfb21a0c 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java @@ -16,6 +16,7 @@ package com.android.server.hdmi; +import static android.content.pm.PackageManager.FEATURE_HDMI_CEC; import static android.media.AudioProfile.AUDIO_ENCAPSULATION_TYPE_NONE; import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; @@ -25,6 +26,7 @@ import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_EARC_PENDING; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; @@ -84,6 +86,9 @@ public class HdmiEarcLocalDeviceTxTest { @Before public void setUp() { + assumeTrue("Test requires FEATURE_HDMI_CEC", + InstrumentationRegistry.getTargetContext().getPackageManager() + .hasSystemFeature(FEATURE_HDMI_CEC)); MockitoAnnotations.initMocks(this); Context context = InstrumentationRegistry.getTargetContext(); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java index 974f64dbd84f..76d4b56f0fb3 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java @@ -16,6 +16,8 @@ package com.android.server.hdmi; +import static android.content.pm.PackageManager.FEATURE_HDMI_CEC; + import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; import static com.android.server.hdmi.Constants.ADDR_BROADCAST; import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1; @@ -23,6 +25,7 @@ import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_2; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; import static org.mockito.Mockito.spy; import android.annotation.RequiresPermission; @@ -64,6 +67,9 @@ public class PowerStatusMonitorActionTest { @Before public void setUp() throws Exception { + assumeTrue("Test requires FEATURE_HDMI_CEC", + InstrumentationRegistry.getTargetContext().getPackageManager() + .hasSystemFeature(FEATURE_HDMI_CEC)); mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); FakeAudioFramework audioFramework = new FakeAudioFramework(); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java index 4cf293758519..02e63f43c6c3 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java @@ -16,12 +16,16 @@ package com.android.server.hdmi; +import static android.content.pm.PackageManager.FEATURE_HDMI_CEC; + import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM; import static com.android.server.hdmi.RequestSadAction.RETRY_COUNTER_MAX; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; + import android.annotation.RequiresPermission; import android.content.Context; import android.content.Intent; @@ -95,6 +99,9 @@ public class RequestSadActionTest { @Before public void setUp() { + assumeTrue("Test requires FEATURE_HDMI_CEC", + InstrumentationRegistry.getTargetContext().getPackageManager() + .hasSystemFeature(FEATURE_HDMI_CEC)); Context context = InstrumentationRegistry.getTargetContext(); mMyLooper = mTestLooper.getLooper(); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ResendCecCommandActionPlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ResendCecCommandActionPlaybackTest.java index 67a3f2a64d32..fa1d3261b84b 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/ResendCecCommandActionPlaybackTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/ResendCecCommandActionPlaybackTest.java @@ -16,11 +16,15 @@ package com.android.server.hdmi; +import static android.content.pm.PackageManager.FEATURE_HDMI_CEC; + import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; import static com.android.server.hdmi.ResendCecCommandAction.SEND_COMMAND_RETRY_MS; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; + import android.annotation.RequiresPermission; import android.content.Context; import android.content.Intent; @@ -58,6 +62,9 @@ public class ResendCecCommandActionPlaybackTest { @Before public void setUp() throws Exception { + assumeTrue("Test requires FEATURE_HDMI_CEC", + InstrumentationRegistry.getTargetContext().getPackageManager() + .hasSystemFeature(FEATURE_HDMI_CEC)); Context context = InstrumentationRegistry.getTargetContext(); FakeAudioFramework audioFramework = new FakeAudioFramework(); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ResendCecCommandActionTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ResendCecCommandActionTvTest.java index 047a04c60176..2f68bab743b3 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/ResendCecCommandActionTvTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/ResendCecCommandActionTvTest.java @@ -16,11 +16,15 @@ package com.android.server.hdmi; +import static android.content.pm.PackageManager.FEATURE_HDMI_CEC; + import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; import static com.android.server.hdmi.ResendCecCommandAction.SEND_COMMAND_RETRY_MS; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; + import android.annotation.RequiresPermission; import android.content.Context; import android.content.Intent; @@ -56,6 +60,9 @@ public class ResendCecCommandActionTvTest { @Before public void setUp() throws Exception { + assumeTrue("Test requires FEATURE_HDMI_CEC", + InstrumentationRegistry.getTargetContext().getPackageManager() + .hasSystemFeature(FEATURE_HDMI_CEC)); Context context = InstrumentationRegistry.getTargetContext(); FakeAudioFramework audioFramework = new FakeAudioFramework(); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java index 1019db46482d..912392f1b70f 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java @@ -16,6 +16,8 @@ package com.android.server.hdmi; +import static android.content.pm.PackageManager.FEATURE_HDMI_CEC; + import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM; import static com.android.server.hdmi.Constants.ADDR_BROADCAST; @@ -29,6 +31,8 @@ import static com.android.server.hdmi.RoutingControlAction.STATE_WAIT_FOR_ROUTIN import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; + import android.annotation.RequiresPermission; import android.content.Context; import android.content.Intent; @@ -143,6 +147,9 @@ public class RoutingControlActionTest { @Before public void setUp() { + assumeTrue("Test requires FEATURE_HDMI_CEC", + InstrumentationRegistry.getTargetContext().getPackageManager() + .hasSystemFeature(FEATURE_HDMI_CEC)); Context context = InstrumentationRegistry.getTargetContext(); mMyLooper = mTestLooper.getLooper(); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java index e4297effed92..a1a5ffe55eaa 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java @@ -16,6 +16,7 @@ package com.android.server.hdmi; +import static android.content.pm.PackageManager.FEATURE_HDMI_CEC; import static android.hardware.hdmi.DeviceFeatures.FEATURE_NOT_SUPPORTED; import static android.hardware.hdmi.DeviceFeatures.FEATURE_SUPPORTED; import static android.hardware.hdmi.DeviceFeatures.FEATURE_SUPPORT_UNKNOWN; @@ -24,6 +25,7 @@ import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doNothing; @@ -79,6 +81,9 @@ public class SetAudioVolumeLevelDiscoveryActionTest { */ @Before public void setUp() throws RemoteException { + assumeTrue("Test requires FEATURE_HDMI_CEC", + InstrumentationRegistry.getTargetContext().getPackageManager() + .hasSystemFeature(FEATURE_HDMI_CEC)); mContextSpy = spy(new ContextWrapper( InstrumentationRegistry.getInstrumentation().getTargetContext())); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java index effea5abcecb..c358e1d4d5db 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java @@ -17,12 +17,15 @@ package com.android.server.hdmi; +import static android.content.pm.PackageManager.FEATURE_HDMI_CEC; + import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM; import static com.android.server.hdmi.SystemAudioAutoInitiationAction.RETRIES_ON_TIMEOUT; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; import static org.mockito.Mockito.spy; import android.annotation.RequiresPermission; @@ -65,6 +68,9 @@ public class SystemAudioAutoInitiationActionTest { @Before public void setUp() throws Exception { + assumeTrue("Test requires FEATURE_HDMI_CEC", + InstrumentationRegistry.getTargetContext().getPackageManager() + .hasSystemFeature(FEATURE_HDMI_CEC)); mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); Looper myLooper = mTestLooper.getLooper(); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 3aa95449cc98..01d34b697def 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -606,7 +606,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Parameters(name = "{0}") public static List<FlagsParameterization> getParams() { - return FlagsParameterization.allCombinationsOf(); + return FlagsParameterization.allCombinationsOf( + FLAG_NOTIFICATION_CLASSIFICATION, FLAG_NM_BINDER_PERF_CACHE_CHANNELS); } public NotificationManagerServiceTest(FlagsParameterization flags) { diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java index 70f57eb40385..3c74ad06a21f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java @@ -19,6 +19,7 @@ package com.android.server.wm; import static android.app.ActivityManager.START_DELIVERED_TO_TOP; import static android.app.ActivityManager.START_TASK_TO_FRONT; import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SECONDARY_DISPLAY; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; @@ -44,20 +45,26 @@ import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.timeout; +import android.annotation.NonNull; import android.app.ActivityOptions; import android.app.WaitResult; +import android.app.WindowConfiguration; import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; +import android.graphics.Rect; import android.os.Binder; import android.os.ConditionVariable; import android.os.IBinder; import android.os.RemoteException; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.view.Display; import androidx.test.filters.MediumTest; +import com.android.window.flags.Flags; + import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentMatchers; @@ -424,4 +431,95 @@ public class ActivityTaskSupervisorTests extends WindowTestsBase { assertThat(activity.mLaunchCookie).isNull(); verify(mAtm).moveTaskToFrontLocked(any(), eq(null), anyInt(), anyInt(), eq(safeOptions)); } + + @Test + public void testOpaque_leafTask_occludingActivity_isOpaque() { + final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build(); + activity.setOccludesParent(true); + final TaskFragment tf = activity.getTaskFragment(); + + assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(tf)).isTrue(); + } + + @Test + public void testOpaque_leafTask_nonOccludingActivity_isTranslucent() { + final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build(); + activity.setOccludesParent(false); + final TaskFragment tf = activity.getTaskFragment(); + + assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(tf)).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + public void testOpaque_rootTask_translucentFillingChild_isTranslucent() { + final Task rootTask = new TaskBuilder(mSupervisor).setOnTop(true).build(); + createChildTaskFragment(/* parent */ rootTask, + WINDOWING_MODE_FREEFORM, /* opaque */ false, /* filling */ true); + + assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(rootTask)).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + public void testOpaque_rootTask_opaqueAndNotFillingChild_isTranslucent() { + final Task rootTask = new TaskBuilder(mSupervisor).setOnTop(true).build(); + createChildTaskFragment(/* parent */ rootTask, + WINDOWING_MODE_FREEFORM, /* opaque */ true, /* filling */ false); + + assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(rootTask)).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + public void testOpaque_rootTask_opaqueAndFillingChild_isOpaque() { + final Task rootTask = new TaskBuilder(mSupervisor).setOnTop(true).build(); + createChildTaskFragment(/* parent */ rootTask, + WINDOWING_MODE_FREEFORM, /* opaque */ true, /* filling */ true); + + assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(rootTask)).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + public void testOpaque_rootTask_nonFillingOpaqueAdjacentChildren_isOpaque() { + final Task rootTask = new TaskBuilder(mSupervisor).setOnTop(true).build(); + final TaskFragment tf1 = createChildTaskFragment(/* parent */ rootTask, + WINDOWING_MODE_MULTI_WINDOW, /* opaque */ true, /* filling */ false); + final TaskFragment tf2 = createChildTaskFragment(/* parent */ rootTask, + WINDOWING_MODE_MULTI_WINDOW, /* opaque */ true, /* filling */ false); + tf1.setAdjacentTaskFragment(tf2); + + assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(rootTask)).isTrue(); + } + + @Test + @EnableFlags({Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS}) + public void testOpaque_rootTask_nonFillingOpaqueAdjacentChildren_multipleAdjacent_isOpaque() { + final Task rootTask = new TaskBuilder(mSupervisor).setOnTop(true).build(); + final TaskFragment tf1 = createChildTaskFragment(/* parent */ rootTask, + WINDOWING_MODE_MULTI_WINDOW, /* opaque */ true, /* filling */ false); + final TaskFragment tf2 = createChildTaskFragment(/* parent */ rootTask, + WINDOWING_MODE_MULTI_WINDOW, /* opaque */ true, /* filling */ false); + final TaskFragment tf3 = createChildTaskFragment(/* parent */ rootTask, + WINDOWING_MODE_MULTI_WINDOW, /* opaque */ true, /* filling */ false); + tf1.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(tf1, tf2, tf3)); + + assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(rootTask)).isTrue(); + } + + @NonNull + private TaskFragment createChildTaskFragment(@NonNull Task parent, + @WindowConfiguration.WindowingMode int windowingMode, + boolean opaque, + boolean filling) { + final ActivityRecord activity = new ActivityBuilder(mAtm) + .setCreateTask(true).setParentTask(parent).build(); + activity.setOccludesParent(opaque); + final TaskFragment tf = activity.getTaskFragment(); + tf.setWindowingMode(windowingMode); + tf.setBounds(filling ? new Rect() : new Rect(100, 100, 200, 200)); + return tf; + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java deleted file mode 100644 index 169968c75fc5..000000000000 --- a/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java +++ /dev/null @@ -1,175 +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.server.wm; - -import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; -import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; -import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; -import static android.view.WindowManager.TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE; - -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.os.IBinder; -import android.platform.test.annotations.Presubmit; -import android.view.Display; -import android.view.IRemoteAnimationFinishedCallback; -import android.view.IRemoteAnimationRunner; -import android.view.RemoteAnimationAdapter; -import android.view.RemoteAnimationDefinition; -import android.view.RemoteAnimationTarget; -import android.view.WindowManager; - -import androidx.test.filters.SmallTest; - -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Tests for change transitions - * - * Build/Install/Run: - * atest WmTests:AppChangeTransitionTests - */ -@SmallTest -@Presubmit -@RunWith(WindowTestRunner.class) -public class AppChangeTransitionTests extends WindowTestsBase { - - private Task mTask; - private ActivityRecord mActivity; - - public void setUpOnDisplay(DisplayContent dc) { - mActivity = createActivityRecord(dc, WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_STANDARD); - mTask = mActivity.getTask(); - - // Set a remote animator with snapshot disabled. Snapshots don't work in wmtests. - RemoteAnimationDefinition definition = new RemoteAnimationDefinition(); - RemoteAnimationAdapter adapter = - new RemoteAnimationAdapter(new TestRemoteAnimationRunner(), 10, 1, false); - definition.addRemoteAnimation(TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE, adapter); - dc.registerRemoteAnimations(definition); - } - - class TestRemoteAnimationRunner implements IRemoteAnimationRunner { - @Override - public void onAnimationStart(@WindowManager.TransitionOldType int transit, - RemoteAnimationTarget[] apps, - RemoteAnimationTarget[] wallpapers, - RemoteAnimationTarget[] nonApps, - IRemoteAnimationFinishedCallback finishedCallback) { - for (RemoteAnimationTarget target : apps) { - assertNotNull(target.startBounds); - } - try { - finishedCallback.onAnimationFinished(); - } catch (Exception e) { - throw new RuntimeException("Something went wrong"); - } - } - - @Override - public void onAnimationCancelled() { - } - - @Override - public IBinder asBinder() { - return null; - } - } - - @Test - public void testModeChangeRemoteAnimatorNoSnapshot() { - // setup currently defaults to no snapshot. - setUpOnDisplay(mDisplayContent); - - mTask.setWindowingMode(WINDOWING_MODE_FREEFORM); - assertEquals(1, mDisplayContent.mChangingContainers.size()); - - // Verify we are in a change transition, but without a snapshot. - // Though, the test will actually have crashed by now if a snapshot is attempted. - assertNull(mTask.mSurfaceFreezer.mSnapshot); - assertTrue(mTask.isInChangeTransition()); - - waitUntilHandlersIdle(); - mActivity.removeImmediately(); - } - - @Test - public void testCancelPendingChangeOnRemove() { - // setup currently defaults to no snapshot. - setUpOnDisplay(mDisplayContent); - - mTask.setWindowingMode(WINDOWING_MODE_FREEFORM); - assertEquals(1, mDisplayContent.mChangingContainers.size()); - assertTrue(mTask.isInChangeTransition()); - - // Removing the app-token from the display should clean-up the - // the change leash. - mDisplayContent.removeAppToken(mActivity.token); - assertEquals(0, mDisplayContent.mChangingContainers.size()); - assertFalse(mTask.isInChangeTransition()); - - waitUntilHandlersIdle(); - mActivity.removeImmediately(); - } - - @Test - public void testNoChangeOnOldDisplayWhenMoveDisplay() { - mDisplayContent.getDefaultTaskDisplayArea().setWindowingMode(WINDOWING_MODE_FULLSCREEN); - final DisplayContent dc1 = createNewDisplay(Display.STATE_ON); - dc1.getDefaultTaskDisplayArea().setWindowingMode(WINDOWING_MODE_FREEFORM); - setUpOnDisplay(dc1); - - assertEquals(WINDOWING_MODE_FREEFORM, mTask.getWindowingMode()); - - // Reparenting to a display with different windowing mode may trigger - // a change transition internally, but it should be cleaned-up once - // the display change is complete. - mTask.reparent(mDisplayContent.getDefaultTaskDisplayArea(), true); - - assertEquals(WINDOWING_MODE_FULLSCREEN, mTask.getWindowingMode()); - - // Make sure the change transition is not the old display - assertFalse(dc1.mChangingContainers.contains(mTask)); - - waitUntilHandlersIdle(); - mActivity.removeImmediately(); - } - - @Test - public void testCancelPendingChangeOnHide() { - // setup currently defaults to no snapshot. - setUpOnDisplay(mDisplayContent); - - mTask.setWindowingMode(WINDOWING_MODE_FREEFORM); - assertEquals(1, mDisplayContent.mChangingContainers.size()); - assertTrue(mTask.isInChangeTransition()); - - // Changing visibility should cancel the change transition and become closing - mActivity.setVisibility(false); - assertEquals(0, mDisplayContent.mChangingContainers.size()); - assertFalse(mTask.isInChangeTransition()); - - waitUntilHandlersIdle(); - mActivity.removeImmediately(); - } -} diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java index 03d904283e83..8553fbd30ab8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java @@ -409,7 +409,6 @@ public class AppTransitionTests extends WindowTestsBase { task.getBounds(taskBounds); taskFragment.setBounds(0, 0, taskBounds.right / 2, taskBounds.bottom); spyOn(taskFragment); - mockSurfaceFreezerSnapshot(taskFragment.mSurfaceFreezer); assertTrue(mDc.mChangingContainers.isEmpty()); assertFalse(mDc.mAppTransition.isTransitionSet()); @@ -422,7 +421,6 @@ public class AppTransitionTests extends WindowTestsBase { verify(taskFragment).initializeChangeTransition(activity.getBounds(), activityLeash); assertTrue(mDc.mChangingContainers.contains(taskFragment)); assertTrue(mDc.mAppTransition.containsTransitRequest(TRANSIT_CHANGE)); - assertEquals(startBounds, taskFragment.mSurfaceFreezer.mFreezeBounds); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index c3aa2894997d..dba463a436c0 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -4461,46 +4461,7 @@ public class SizeCompatTests extends WindowTestsBase { // are aligned to the top of the parentAppBounds assertEquals(new Rect(0, notchHeight, 1000, 1200), appBounds); assertEquals(new Rect(0, 0, 1000, 1200), bounds); - } - - @Test - @DisableCompatChanges({ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED}) - public void testInFreeform_boundsSandboxedToAppBounds() { - final int dw = 2800; - final int dh = 1400; - final int notchHeight = 100; - final DisplayContent display = new TestDisplayContent.Builder(mAtm, dw, dh) - .setNotch(notchHeight) - .build(); - setUpApp(display); - prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); - mTask.mDisplayContent.getDefaultTaskDisplayArea() - .setWindowingMode(WindowConfiguration.WINDOWING_MODE_FREEFORM); - mTask.setWindowingMode(WINDOWING_MODE_FREEFORM); - Rect appBounds = new Rect(0, 0, 1000, 500); - Rect bounds = new Rect(0, 0, 1000, 600); - mTask.getWindowConfiguration().setAppBounds(appBounds); - mTask.getWindowConfiguration().setBounds(bounds); - mActivity.onConfigurationChanged(mTask.getConfiguration()); - - // Bounds are sandboxed to appBounds in freeform. - assertDownScaled(); - assertEquals(mActivity.getWindowConfiguration().getAppBounds(), - mActivity.getWindowConfiguration().getBounds()); - - // Exit freeform. - mTask.mDisplayContent.getDefaultTaskDisplayArea() - .setWindowingMode(WindowConfiguration.WINDOWING_MODE_FULLSCREEN); - mTask.setWindowingMode(WINDOWING_MODE_FULLSCREEN); - mTask.getWindowConfiguration().setBounds(new Rect(0, 0, dw, dh)); - mActivity.onConfigurationChanged(mTask.getConfiguration()); - assertFitted(); - appBounds = mActivity.getWindowConfiguration().getAppBounds(); - bounds = mActivity.getWindowConfiguration().getBounds(); - // Bounds are not sandboxed to appBounds. - assertNotEquals(appBounds, bounds); - assertEquals(notchHeight, appBounds.top - bounds.top); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java index 0d9772492e59..ee8d7308f6b3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java @@ -152,7 +152,6 @@ public class TaskFragmentTest extends WindowTestsBase { ACTIVITY_TYPE_STANDARD); task.setBoundsUnchecked(new Rect(0, 0, 1000, 1000)); mTaskFragment = createTaskFragmentWithEmbeddedActivity(task, mOrganizer); - mockSurfaceFreezerSnapshot(mTaskFragment.mSurfaceFreezer); final Rect startBounds = new Rect(0, 0, 500, 1000); final Rect endBounds = new Rect(500, 0, 1000, 1000); mTaskFragment.setRelativeEmbeddedBounds(startBounds); @@ -179,44 +178,6 @@ public class TaskFragmentTest extends WindowTestsBase { } @Test - public void testStartChangeTransition_resetSurface() { - final Task task = createTask(mDisplayContent, WINDOWING_MODE_MULTI_WINDOW, - ACTIVITY_TYPE_STANDARD); - task.setBoundsUnchecked(new Rect(0, 0, 1000, 1000)); - mTaskFragment = createTaskFragmentWithEmbeddedActivity(task, mOrganizer); - doReturn(mTransaction).when(mTaskFragment).getSyncTransaction(); - doReturn(mTransaction).when(mTaskFragment).getPendingTransaction(); - mLeash = mTaskFragment.getSurfaceControl(); - mockSurfaceFreezerSnapshot(mTaskFragment.mSurfaceFreezer); - final Rect startBounds = new Rect(0, 0, 1000, 1000); - final Rect endBounds = new Rect(500, 500, 1000, 1000); - mTaskFragment.setRelativeEmbeddedBounds(startBounds); - mTaskFragment.recomputeConfiguration(); - doReturn(true).when(mTaskFragment).isVisible(); - doReturn(true).when(mTaskFragment).isVisibleRequested(); - - clearInvocations(mTransaction); - final Rect relStartBounds = new Rect(mTaskFragment.getRelativeEmbeddedBounds()); - mTaskFragment.deferOrganizedTaskFragmentSurfaceUpdate(); - mTaskFragment.setRelativeEmbeddedBounds(endBounds); - mTaskFragment.recomputeConfiguration(); - assertTrue(mTaskFragment.shouldStartChangeTransition(startBounds, relStartBounds)); - mTaskFragment.initializeChangeTransition(startBounds); - mTaskFragment.continueOrganizedTaskFragmentSurfaceUpdate(); - - // Surface reset when prepare transition. - verify(mTransaction).setPosition(mLeash, 0, 0); - verify(mTransaction).setWindowCrop(mLeash, 0, 0); - - clearInvocations(mTransaction); - mTaskFragment.mSurfaceFreezer.unfreeze(mTransaction); - - // Update surface after animation. - verify(mTransaction).setPosition(mLeash, 500, 500); - verify(mTransaction).setWindowCrop(mLeash, 500, 500); - } - - @Test public void testStartChangeTransition_doNotFreezeWhenOnlyMoved() { final Rect startBounds = new Rect(0, 0, 1000, 1000); final Rect endBounds = new Rect(startBounds); @@ -235,7 +196,6 @@ public class TaskFragmentTest extends WindowTestsBase { @Test public void testNotOkToAnimate_doNotStartChangeTransition() { - mockSurfaceFreezerSnapshot(mTaskFragment.mSurfaceFreezer); final Rect startBounds = new Rect(0, 0, 1000, 1000); final Rect endBounds = new Rect(500, 500, 1000, 1000); mTaskFragment.setRelativeEmbeddedBounds(startBounds); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java index cc447a18758c..001446550304 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java @@ -32,7 +32,6 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OLD_TASK_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE; import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN; import static android.view.WindowManager.TRANSIT_OPEN; import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER; @@ -64,7 +63,6 @@ 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.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -1014,30 +1012,6 @@ public class WindowContainerTests extends WindowTestsBase { } @Test - public void testOnDisplayChanged_cleanupChanging() { - final Task task = createTask(mDisplayContent); - addLocalInsets(task); - spyOn(task.mSurfaceFreezer); - mDisplayContent.mChangingContainers.add(task); - - // Don't remove the changing transition of this window when it is still the old display. - // This happens on display info changed. - task.onDisplayChanged(mDisplayContent); - - assertTrue(task.mLocalInsetsSources.size() == 1); - assertTrue(mDisplayContent.mChangingContainers.contains(task)); - verify(task.mSurfaceFreezer, never()).unfreeze(any()); - - // Remove the changing transition of this window when it is moved or reparented from the old - // display. - final DisplayContent newDc = createNewDisplay(); - task.onDisplayChanged(newDc); - - assertFalse(mDisplayContent.mChangingContainers.contains(task)); - verify(task.mSurfaceFreezer).unfreeze(any()); - } - - @Test public void testHandleCompleteDeferredRemoval() { final DisplayContent displayContent = createNewDisplay(); // Do not reparent activity to default display when removing the display. @@ -1290,157 +1264,17 @@ public class WindowContainerTests extends WindowTestsBase { final WindowContainer container = new WindowContainer(mWm); container.mSurfaceControl = mock(SurfaceControl.class); final SurfaceAnimator surfaceAnimator = container.mSurfaceAnimator; - final SurfaceFreezer surfaceFreezer = container.mSurfaceFreezer; final SurfaceControl relativeParent = mock(SurfaceControl.class); final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class); spyOn(container); spyOn(surfaceAnimator); - spyOn(surfaceFreezer); doReturn(t).when(container).getSyncTransaction(); container.setLayer(t, 1); container.setRelativeLayer(t, relativeParent, 2); - // Set through surfaceAnimator if surfaceFreezer doesn't have leash. verify(surfaceAnimator).setLayer(t, 1); verify(surfaceAnimator).setRelativeLayer(t, relativeParent, 2); - verify(surfaceFreezer, never()).setLayer(any(), anyInt()); - verify(surfaceFreezer, never()).setRelativeLayer(any(), any(), anyInt()); - - clearInvocations(surfaceAnimator); - clearInvocations(surfaceFreezer); - doReturn(true).when(surfaceFreezer).hasLeash(); - - container.setLayer(t, 1); - container.setRelativeLayer(t, relativeParent, 2); - - // Set through surfaceFreezer if surfaceFreezer has leash. - verify(surfaceFreezer).setLayer(t, 1); - verify(surfaceFreezer).setRelativeLayer(t, relativeParent, 2); - verify(surfaceAnimator, never()).setLayer(any(), anyInt()); - verify(surfaceAnimator, never()).setRelativeLayer(any(), any(), anyInt()); - } - - @Test - public void testStartChangeTransitionWhenPreviousIsNotFinished() { - final WindowContainer container = createTaskFragmentWithActivity( - createTask(mDisplayContent)); - container.mSurfaceControl = mock(SurfaceControl.class); - final SurfaceAnimator surfaceAnimator = container.mSurfaceAnimator; - final SurfaceFreezer surfaceFreezer = container.mSurfaceFreezer; - final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class); - spyOn(container); - spyOn(surfaceAnimator); - mockSurfaceFreezerSnapshot(surfaceFreezer); - doReturn(t).when(container).getPendingTransaction(); - doReturn(t).when(container).getSyncTransaction(); - - // Leash and snapshot created for change transition. - container.initializeChangeTransition(new Rect(0, 0, 1000, 2000)); - - assertNotNull(surfaceFreezer.mLeash); - assertNotNull(surfaceFreezer.mSnapshot); - assertEquals(surfaceFreezer.mLeash, container.getAnimationLeash()); - - // Start animation: surfaceAnimator take over the leash and snapshot from surfaceFreezer. - container.applyAnimationUnchecked(null /* lp */, true /* enter */, - TRANSIT_OLD_TASK_FRAGMENT_CHANGE, false /* isVoiceInteraction */, - null /* sources */); - - assertNull(surfaceFreezer.mLeash); - assertNull(surfaceFreezer.mSnapshot); - assertNotNull(surfaceAnimator.mLeash); - assertNotNull(surfaceAnimator.mSnapshot); - final SurfaceControl prevLeash = surfaceAnimator.mLeash; - final SurfaceFreezer.Snapshot prevSnapshot = surfaceAnimator.mSnapshot; - - // Prepare another change transition. - container.initializeChangeTransition(new Rect(0, 0, 1000, 2000)); - - assertNotNull(surfaceFreezer.mLeash); - assertNotNull(surfaceFreezer.mSnapshot); - assertEquals(surfaceFreezer.mLeash, container.getAnimationLeash()); - assertNotEquals(prevLeash, container.getAnimationLeash()); - - // Start another animation before the previous one is finished, it should reset the previous - // one, but not change the current one. - container.applyAnimationUnchecked(null /* lp */, true /* enter */, - TRANSIT_OLD_TASK_FRAGMENT_CHANGE, false /* isVoiceInteraction */, - null /* sources */); - - verify(container, never()).onAnimationLeashLost(any()); - verify(surfaceFreezer, never()).unfreeze(any()); - assertNotNull(surfaceAnimator.mLeash); - assertNotNull(surfaceAnimator.mSnapshot); - assertEquals(surfaceAnimator.mLeash, container.getAnimationLeash()); - assertNotEquals(prevLeash, surfaceAnimator.mLeash); - assertNotEquals(prevSnapshot, surfaceAnimator.mSnapshot); - - // Clean up after animation finished. - surfaceAnimator.mInnerAnimationFinishedCallback.onAnimationFinished( - ANIMATION_TYPE_APP_TRANSITION, surfaceAnimator.getAnimation()); - - verify(container).onAnimationLeashLost(any()); - assertNull(surfaceAnimator.mLeash); - assertNull(surfaceAnimator.mSnapshot); - } - - @Test - public void testUnfreezeWindow_removeWindowFromChanging() { - final WindowContainer container = createTaskFragmentWithActivity( - createTask(mDisplayContent)); - mockSurfaceFreezerSnapshot(container.mSurfaceFreezer); - final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class); - - container.initializeChangeTransition(new Rect(0, 0, 1000, 2000)); - - assertTrue(mDisplayContent.mChangingContainers.contains(container)); - - container.mSurfaceFreezer.unfreeze(t); - - assertFalse(mDisplayContent.mChangingContainers.contains(container)); - } - - @Test - public void testFailToTaskSnapshot_unfreezeWindow() { - final WindowContainer container = createTaskFragmentWithActivity( - createTask(mDisplayContent)); - final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class); - spyOn(container.mSurfaceFreezer); - - container.initializeChangeTransition(new Rect(0, 0, 1000, 2000)); - - verify(container.mSurfaceFreezer).freeze(any(), any(), any(), any()); - verify(container.mSurfaceFreezer).unfreeze(any()); - assertTrue(mDisplayContent.mChangingContainers.isEmpty()); - } - - @Test - public void testRemoveUnstartedFreezeSurfaceWhenFreezeAgain() { - final WindowContainer container = createTaskFragmentWithActivity( - createTask(mDisplayContent)); - container.mSurfaceControl = mock(SurfaceControl.class); - final SurfaceFreezer surfaceFreezer = container.mSurfaceFreezer; - mockSurfaceFreezerSnapshot(surfaceFreezer); - final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class); - spyOn(container); - doReturn(t).when(container).getPendingTransaction(); - doReturn(t).when(container).getSyncTransaction(); - - // Leash and snapshot created for change transition. - container.initializeChangeTransition(new Rect(0, 0, 1000, 2000)); - - assertNotNull(surfaceFreezer.mLeash); - assertNotNull(surfaceFreezer.mSnapshot); - - final SurfaceControl prevLeash = surfaceFreezer.mLeash; - final SurfaceFreezer.Snapshot prevSnapshot = surfaceFreezer.mSnapshot; - spyOn(prevSnapshot); - - container.initializeChangeTransition(new Rect(0, 0, 1500, 2500)); - - verify(t).remove(prevLeash); - verify(prevSnapshot).destroy(t); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index 2c390c504e9f..b16f5283d532 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -78,7 +78,6 @@ import android.content.pm.ApplicationInfo; import android.content.res.Configuration; import android.graphics.Insets; import android.graphics.Rect; -import android.hardware.HardwareBuffer; import android.hardware.display.DisplayManager; import android.os.Binder; import android.os.Build; @@ -113,7 +112,6 @@ import android.window.ActivityWindowInfo; import android.window.ClientWindowFrames; import android.window.ITaskFragmentOrganizer; import android.window.ITransitionPlayer; -import android.window.ScreenCapture; import android.window.StartingWindowInfo; import android.window.StartingWindowRemovalInfo; import android.window.TaskFragmentOrganizer; @@ -1112,21 +1110,6 @@ public class WindowTestsBase extends SystemServiceTestsBase { displayContent -> displayContent.mMinSizeOfResizeableTaskDp = 1); } - /** Mocks the behavior of taking a snapshot. */ - void mockSurfaceFreezerSnapshot(SurfaceFreezer surfaceFreezer) { - final ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer = - mock(ScreenCapture.ScreenshotHardwareBuffer.class); - final HardwareBuffer hardwareBuffer = mock(HardwareBuffer.class); - spyOn(surfaceFreezer); - doReturn(screenshotBuffer).when(surfaceFreezer) - .createSnapshotBufferInner(any(), any()); - doReturn(null).when(surfaceFreezer) - .createFromHardwareBufferInner(any()); - doReturn(hardwareBuffer).when(screenshotBuffer).getHardwareBuffer(); - doReturn(100).when(hardwareBuffer).getWidth(); - doReturn(100).when(hardwareBuffer).getHeight(); - } - static ComponentName getUniqueComponentName() { return getUniqueComponentName(DEFAULT_COMPONENT_PACKAGE_NAME); } |