diff options
298 files changed, 7212 insertions, 3683 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/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index 3fb08224b9db..bc01f934e701 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -270,6 +270,7 @@ interface INotificationManager int[] getAllowedAdjustmentKeyTypes(); void setAssistantAdjustmentKeyTypeState(int type, boolean enabled); + String[] getAdjustmentDeniedPackages(String key); boolean isAdjustmentSupportedForPackage(String key, String pkg); void setAdjustmentSupportedForPackage(String key, String pkg, boolean enabled); diff --git a/core/java/android/app/contextualsearch/flags.aconfig b/core/java/android/app/contextualsearch/flags.aconfig index c19921dcdc61..d81ec1e8b883 100644 --- a/core/java/android/app/contextualsearch/flags.aconfig +++ b/core/java/android/app/contextualsearch/flags.aconfig @@ -27,7 +27,10 @@ flag { name: "contextual_search_window_layer" namespace: "sysui_integrations" description: "Identify live contextual search UI to exclude from contextual search screenshot." - bug: "372510690" + bug: "390176823" + metadata { + purpose: PURPOSE_BUGFIX + } } flag { @@ -35,4 +38,4 @@ flag { namespace: "sysui_integrations" description: "Add audio playing status to the contextual search invocation intent." bug: "372935419" -}
\ No newline at end of file +} 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/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/os/UserManager.java b/core/java/android/os/UserManager.java index aaf6489d18ca..ce93c71ac776 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -5326,7 +5326,9 @@ public class UserManager { } /** - * Returns list of the profiles of userId including userId itself. + * Returns a list of the users that are associated with userId, including userId itself. This + * includes the user, its profiles, its parent, and its parent's other profiles, as applicable. + * * Note that this returns both enabled and not enabled profiles. See * {@link #getEnabledProfiles(int)} if you need only the enabled ones. * <p>Note that this includes all profile types (not including Restricted profiles). @@ -5334,7 +5336,7 @@ public class UserManager { * <p>Requires {@link android.Manifest.permission#MANAGE_USERS} or * {@link android.Manifest.permission#CREATE_USERS} or * {@link android.Manifest.permission#QUERY_USERS} if userId is not the calling user. - * @param userId profiles of this user will be returned. + * @param userId profiles associated with this user (including itself) will be returned. * @return the list of profiles. * @hide */ @@ -5358,12 +5360,13 @@ public class UserManager { } /** - * Returns list of the profiles of the given user, including userId itself, as well as the - * communal profile, if there is one. + * Returns a list of the users that are associated with userId, including userId itself, + * as well as the communal profile, if there is one. * * <p>Note that this returns both enabled and not enabled profiles. * <p>Note that this includes all profile types (not including Restricted profiles). * + * @see #getProfiles(int) * @hide */ @FlaggedApi(android.multiuser.Flags.FLAG_SUPPORT_COMMUNAL_PROFILE) @@ -5419,7 +5422,10 @@ public class UserManager { } /** - * Returns list of the profiles of userId including userId itself. + * Returns a list of the enabled users that are associated with userId, including userId itself. + * This includes the user, its profiles, its parent, and its parent's other profiles, as + * applicable. + * * Note that this returns only {@link UserInfo#isEnabled() enabled} profiles. * <p>Note that this includes all profile types (not including Restricted profiles). * @@ -5447,8 +5453,10 @@ public class UserManager { } /** - * Returns a list of UserHandles for profiles associated with the context user, including the - * user itself. + * Returns a list of the users that are associated with the context user, including the user + * itself. This includes the user, its profiles, its parent, and its parent's other profiles, + * as applicable. + * * <p>Note that this includes all profile types (not including Restricted profiles). * * @return A non-empty list of UserHandles associated with the context user. @@ -5465,8 +5473,10 @@ public class UserManager { } /** - * Returns a list of ids for enabled profiles associated with the context user including the - * user itself. + * Returns a list of the enabled users that are associated with the context user, including the + * user itself. This includes the user, its profiles, its parent, and its parent's other + * profiles, as applicable. + * * <p>Note that this includes all profile types (not including Restricted profiles). * * @return A non-empty list of UserHandles associated with the context user. @@ -5483,8 +5493,10 @@ public class UserManager { } /** - * Returns a list of ids for all profiles associated with the context user including the user - * itself. + * Returns a list of all users that are associated with the context user, including the user + * itself. This includes the user, its profiles, its parent, and its parent's other profiles, + * as applicable. + * * <p>Note that this includes all profile types (not including Restricted profiles). * * @return A non-empty list of UserHandles associated with the context user. @@ -5501,8 +5513,10 @@ public class UserManager { } /** - * Returns a list of ids for profiles associated with the context user including the user - * itself. + * Returns a list of the users that are associated with the context user, including the user + * itself. This includes the user, its profiles, its parent, and its parent's other profiles, as + * applicable. + * * <p>Note that this includes all profile types (not including Restricted profiles). * * @param enabledOnly whether to return only {@link UserInfo#isEnabled() enabled} profiles @@ -5528,8 +5542,10 @@ public class UserManager { } /** - * Returns a list of ids for profiles associated with the specified user including the user - * itself. + * Returns a list of the users that are associated with the specified user, including the user + * itself. This includes the user, its profiles, its parent, and its parent's other profiles, + * as applicable. + * * <p>Note that this includes all profile types (not including Restricted profiles). * * @param userId id of the user to return profiles for 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 f86a4249c0e1..082bf5dc5a1c 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -77,7 +77,7 @@ public enum DesktopModeFlags { ENABLE_DESKTOP_WINDOWING_MODE(Flags::enableDesktopWindowingMode, true), ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES( Flags::enableDesktopWindowingMultiInstanceFeatures, true), - ENABLE_DESKTOP_WINDOWING_PERSISTENCE(Flags::enableDesktopWindowingPersistence, false), + ENABLE_DESKTOP_WINDOWING_PERSISTENCE(Flags::enableDesktopWindowingPersistence, true), ENABLE_DESKTOP_WINDOWING_QUICK_SWITCH(Flags::enableDesktopWindowingQuickSwitch, true), ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS(Flags::enableDesktopWindowingSizeConstraints, true), ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS(Flags::enableDesktopWindowingTaskbarRunningApps, 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/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/config.xml b/core/res/res/values/config.xml index a49e03484192..9f731fe04472 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -5958,6 +5958,9 @@ <!-- <item>com.google</item> --> </string-array> + <!-- Whether to restrict the accounts that raw contacts can be created in. --> + <bool name = "config_rawContactsAccountRestrictionEnabled">true</bool> + <!-- Whether or not to use assistant stream volume separately from music volume --> <bool name="config_useAssistantVolume">false</bool> 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/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 653996afbc01..f4004fa70623 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4617,6 +4617,7 @@ <java-symbol type="string" name="config_rawContactsLocalAccountName" /> <java-symbol type="string" name="config_rawContactsLocalAccountType" /> <java-symbol type="array" name="config_rawContactsEligibleDefaultAccountTypes" /> + <java-symbol type="bool" name="config_rawContactsAccountRestrictionEnabled" /> <!-- For App Standby --> <java-symbol type="string" name="as_app_forced_to_restricted_bucket" /> 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 ea0894bf1eea..deec52d1c19e 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 @@ -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); } @@ -232,7 +231,7 @@ public class DesktopModeStatus { * Return {@code true} if desktop mode dev option should be shown on current device */ public static boolean canShowDesktopExperienceDevOption(@NonNull Context context) { - return Flags.showDesktopExperienceDevOption(); + return Flags.showDesktopExperienceDevOption() && isDeviceEligibleForDesktopMode(context); } /** Returns if desktop mode dev option should be enabled if there is no user override. */ 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/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/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt index cc3d86c0c056..2ac76f319d32 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt @@ -959,9 +959,16 @@ constructor( super.setupEndDragToDesktop(info, startTransaction, finishTransaction) val state = requireTransitionState() - val homeLeash = state.homeChange?.leash ?: error("Expects home leash to be non-null") - // Hide home on finish to prevent flickering when wallpaper activity flag is enabled - finishTransaction.hide(homeLeash) + val homeLeash = state.homeChange?.leash + if (homeLeash == null) { + ProtoLog.e( + ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "DragToDesktop: home leash is null", + ) + } else { + // Hide home on finish to prevent flickering when wallpaper activity flag is enabled + finishTransaction.hide(homeLeash) + } // Setup freeform tasks before animation state.freeformTaskChanges.forEach { change -> val startScale = FREEFORM_TASKS_INITIAL_SCALE 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/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/tests/unittest/src/com/android/wm/shell/compatui/CompatUIShellTestCase.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitMultiDisplayProvider.java index 979cee9d63c2..d2e57e51762b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIShellTestCase.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitMultiDisplayProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 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. @@ -14,12 +14,16 @@ * limitations under the License. */ -package com.android.wm.shell.compatui; +package com.android.wm.shell.splitscreen; -import com.android.wm.shell.ShellTestCase; +import android.window.WindowContainerToken; -/** - * Base class for CompatUI tests. - */ -public class CompatUIShellTestCase extends ShellTestCase { +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..17d619c9bee8 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(); 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/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt index 8d04749d76a5..2115f70faad0 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt @@ -50,7 +50,7 @@ constructor( @Test open fun enterDesktopWithDrag() { // By default this method uses drag to desktop - testApp.enterDesktopMode(wmHelper, device) + testApp.enterDesktopMode(wmHelper, device, shouldUseDragToDesktop = true) } @After diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDragExistingWindows.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDragExistingWindows.kt index 814478af67c1..9a1919304675 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDragExistingWindows.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDragExistingWindows.kt @@ -62,7 +62,7 @@ constructor( @Test open fun reenterDesktopWithDrag() { // By default this method uses drag to desktop - testApp.enterDesktopMode(wmHelper, device) + testApp.enterDesktopMode(wmHelper, device, shouldUseDragToDesktop = true) } @After diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java index b5c9fa151dac..2264adec9a19 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java @@ -49,6 +49,7 @@ import androidx.test.filters.SmallTest; import com.android.window.flags.Flags; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; @@ -85,7 +86,7 @@ import org.mockito.MockitoAnnotations; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class CompatUIControllerTest extends CompatUIShellTestCase { +public class CompatUIControllerTest extends ShellTestCase { private static final int DISPLAY_ID = 0; private static final int TASK_ID = 12; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java index 2117b062bf57..c567b5fbbb70 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java @@ -38,6 +38,7 @@ import androidx.test.filters.SmallTest; import com.android.window.flags.Flags; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState; @@ -62,7 +63,7 @@ import java.util.function.Consumer; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class CompatUILayoutTest extends CompatUIShellTestCase { +public class CompatUILayoutTest extends ShellTestCase { private static final int TASK_ID = 1; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java index 0b37648faeec..8fd7c0ec3099 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java @@ -27,6 +27,8 @@ import android.testing.AndroidTestingRunner; import androidx.test.filters.SmallTest; +import com.android.wm.shell.ShellTestCase; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -42,7 +44,7 @@ import java.util.function.IntSupplier; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class CompatUIStatusManagerTest extends CompatUIShellTestCase { +public class CompatUIStatusManagerTest extends ShellTestCase { private FakeCompatUIStatusManagerTest mTestState; private CompatUIStatusManager mStatusManager; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java index 010474e42195..0562bb835671 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java @@ -53,6 +53,7 @@ import androidx.test.filters.SmallTest; import com.android.window.flags.Flags; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState; @@ -77,7 +78,7 @@ import java.util.function.Consumer; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class CompatUIWindowManagerTest extends CompatUIShellTestCase { +public class CompatUIWindowManagerTest extends ShellTestCase { private static final int TASK_ID = 1; private static final int TASK_WIDTH = 2000; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java index e786fef1855c..c6884ea17302 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java @@ -32,6 +32,7 @@ import android.view.View; import androidx.test.filters.SmallTest; import com.android.wm.shell.R; +import com.android.wm.shell.ShellTestCase; import org.junit.Before; import org.junit.Test; @@ -47,7 +48,7 @@ import org.mockito.MockitoAnnotations; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class LetterboxEduDialogLayoutTest extends CompatUIShellTestCase { +public class LetterboxEduDialogLayoutTest extends ShellTestCase { @Mock private Runnable mDismissCallback; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java index 09fc082a63e3..cbf5d1bb65dd 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java @@ -62,6 +62,7 @@ import androidx.test.filters.SmallTest; import com.android.window.flags.Flags; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.DockStateReader; @@ -90,7 +91,7 @@ import java.util.function.Consumer; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class LetterboxEduWindowManagerTest extends CompatUIShellTestCase { +public class LetterboxEduWindowManagerTest extends ShellTestCase { private static final int USER_ID_1 = 1; private static final int USER_ID_2 = 2; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java index 02c099b3cfb2..31ea8f76359f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java @@ -34,6 +34,7 @@ import android.view.View; import androidx.test.filters.SmallTest; import com.android.wm.shell.R; +import com.android.wm.shell.ShellTestCase; import org.junit.Before; import org.junit.Test; @@ -50,7 +51,7 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidTestingRunner.class) @SmallTest @TestableLooper.RunWithLooper(setAsMainLooper = true) -public class ReachabilityEduLayoutTest extends CompatUIShellTestCase { +public class ReachabilityEduLayoutTest extends ShellTestCase { private ReachabilityEduLayout mLayout; private View mMoveUpButton; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java index fa04e070250e..1b2c0944777e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java @@ -30,6 +30,7 @@ import android.testing.AndroidTestingRunner; import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; @@ -52,7 +53,7 @@ import java.util.function.BiConsumer; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class ReachabilityEduWindowManagerTest extends CompatUIShellTestCase { +public class ReachabilityEduWindowManagerTest extends ShellTestCase { @Mock private SyncTransactionQueue mSyncTransactionQueue; @Mock diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java index 2cded9d9776c..5075453d8c73 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java @@ -34,6 +34,7 @@ import android.widget.CheckBox; import androidx.test.filters.SmallTest; import com.android.wm.shell.R; +import com.android.wm.shell.ShellTestCase; import org.junit.Before; import org.junit.Test; @@ -51,7 +52,7 @@ import java.util.function.Consumer; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class RestartDialogLayoutTest extends CompatUIShellTestCase { +public class RestartDialogLayoutTest extends ShellTestCase { @Mock private Runnable mDismissCallback; @Mock private Consumer<Boolean> mRestartCallback; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java index ebd0f412a0a1..779a5ca10648 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java @@ -28,6 +28,7 @@ import android.util.Pair; import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.transition.Transitions; @@ -50,7 +51,7 @@ import java.util.function.Consumer; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class RestartDialogWindowManagerTest extends CompatUIShellTestCase { +public class RestartDialogWindowManagerTest extends ShellTestCase { @Mock private SyncTransactionQueue mSyncTransactionQueue; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java index c6532e13f3cc..2b4d5f125783 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java @@ -38,6 +38,7 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; @@ -62,7 +63,7 @@ import java.util.function.BiConsumer; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class UserAspectRatioSettingsLayoutTest extends CompatUIShellTestCase { +public class UserAspectRatioSettingsLayoutTest extends ShellTestCase { private static final int TASK_ID = 1; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java index 096e900199ba..af7c1f5d7692 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java @@ -54,6 +54,7 @@ import android.view.View; import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; @@ -83,7 +84,7 @@ import java.util.function.Supplier; @RunWith(AndroidTestingRunner.class) @RunWithLooper @SmallTest -public class UserAspectRatioSettingsWindowManagerTest extends CompatUIShellTestCase { +public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase { private static final int TASK_ID = 1; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt index 09ffd946ea19..d6b13610c9c1 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt @@ -113,7 +113,7 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { .strictness(Strictness.LENIENT) .spyStatic(DesktopModeStatus::class.java) .startMocking() - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) } testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) shellInit = spy(ShellInit(testExecutor)) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt index 470c110fd49b..403d468a7034 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt @@ -112,7 +112,7 @@ class DesktopModeKeyGestureHandlerTest : ShellTestCase() { .strictness(Strictness.LENIENT) .spyStatic(DesktopModeStatus::class.java) .startMocking() - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) } testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) shellInit = spy(ShellInit(testExecutor)) 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 0428ba58240d..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 @@ -290,7 +291,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() .spyStatic(DesktopModeStatus::class.java) .spyStatic(Toast::class.java) .startMocking() - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) } testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) shellInit = spy(ShellInit(testExecutor)) @@ -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/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt index 33dc1aadf548..25246d9984c3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt @@ -477,6 +477,40 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { } @Test + fun mergeAnimation_endTransition_springHandler_noStartHomeChange_doesntCrash() { + whenever(dragAnimator.computeCurrentVelocity()).thenReturn(PointF()) + val playingFinishTransaction = mock<SurfaceControl.Transaction>() + val mergedStartTransaction = mock<SurfaceControl.Transaction>() + val mergedFinishTransaction = mock<SurfaceControl.Transaction>() + val finishCallback = mock<Transitions.TransitionFinishCallback>() + val task = createTask() + val startTransition = startDrag( + springHandler, task, finishTransaction = playingFinishTransaction, homeChange = null) + springHandler.onTaskResizeAnimationListener = mock() + + springHandler.mergeAnimation( + transition = mock<IBinder>(), + info = + createTransitionInfo( + type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, + draggedTask = task, + ), + startT = mergedStartTransaction, + finishT = mergedFinishTransaction, + mergeTarget = startTransition, + finishCallback = finishCallback, + ) + + // Should show dragged task layer in start and finish transaction + verify(mergedStartTransaction).show(draggedTaskLeash) + verify(playingFinishTransaction).show(draggedTaskLeash) + // Should update the dragged task layer + verify(mergedStartTransaction).setLayer(eq(draggedTaskLeash), anyInt()) + // Should merge animation + verify(finishCallback).onTransitionFinished(null) + } + + @Test fun propertyValue_returnsSystemPropertyValue() { val name = "property_name" val value = 10f @@ -589,6 +623,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { handler: DragToDesktopTransitionHandler, task: RunningTaskInfo = createTask(), finishTransaction: SurfaceControl.Transaction = mock(), + homeChange: TransitionInfo.Change? = createHomeChange(), ): IBinder { whenever(dragAnimator.position).thenReturn(PointF()) // Simulate transition is started and is ready to animate. @@ -599,6 +634,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { createTransitionInfo( type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, draggedTask = task, + homeChange = homeChange, ), startTransaction = mock(), finishTransaction = finishTransaction, @@ -684,16 +720,12 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { } } - private fun createTransitionInfo(type: Int, draggedTask: RunningTaskInfo) = + private fun createTransitionInfo( + type: Int, + draggedTask: RunningTaskInfo, + homeChange: TransitionInfo.Change? = createHomeChange()) = TransitionInfo(type, /* flags= */ 0).apply { - addChange( // Home. - TransitionInfo.Change(mock(), homeTaskLeash).apply { - parent = null - taskInfo = - TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build() - flags = flags or FLAG_IS_WALLPAPER - } - ) + homeChange?.let { addChange(it) } addChange( // Dragged Task. TransitionInfo.Change(mock(), draggedTaskLeash).apply { parent = null @@ -709,6 +741,12 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { ) } + private fun createHomeChange() = TransitionInfo.Change(mock(), homeTaskLeash).apply { + parent = null + taskInfo = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build() + flags = flags or FLAG_IS_WALLPAPER + } + private fun systemPropertiesKey(name: String) = "${SpringDragToDesktopTransitionHandler.SYSTEM_PROPERTIES_GROUP}.$name" } 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/shared/desktopmode/DesktopModeCompatPolicyTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt index a8a7be8fe7e3..d9791bb43489 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt @@ -21,7 +21,7 @@ import android.content.pm.PackageManager import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.internal.R -import com.android.wm.shell.compatui.CompatUIShellTestCase +import com.android.wm.shell.ShellTestCase import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -39,7 +39,7 @@ import org.mockito.kotlin.whenever */ @RunWith(AndroidTestingRunner::class) @SmallTest -class DesktopModeCompatPolicyTest : CompatUIShellTestCase() { +class DesktopModeCompatPolicyTest : ShellTestCase() { private lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy @Before diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt index 4dac99b14aaf..33f14acd0f02 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt @@ -39,6 +39,9 @@ import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.whenever +/** + * Test class for [DesktopModeStatus]. + */ @SmallTest @Presubmit @EnableFlags(Flags.FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) @@ -56,6 +59,7 @@ class DesktopModeStatusTest : ShellTestCase() { doReturn(false).whenever(mockResources).getBoolean( eq(R.bool.config_isDesktopModeDevOptionSupported) ) + setDeviceEligibleForDesktopMode(false) doReturn(context.contentResolver).whenever(mockContext).contentResolver resetDesktopModeFlagsCache() resetEnforceDeviceRestriction() @@ -74,7 +78,7 @@ class DesktopModeStatusTest : ShellTestCase() { Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION ) @Test - fun canEnterDesktopMode_DWFlagDisabled_configsOff_returnsFalse() { + fun canEnterDesktopMode_DWFlagDisabled_deviceNotEligible_returnsFalse() { assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isFalse() } @@ -83,8 +87,8 @@ class DesktopModeStatusTest : ShellTestCase() { Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION ) @Test - fun canEnterDesktopMode_DWFlagDisabled_configsOn_disableDeviceRestrictions_returnsFalse() { - doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)) + fun canEnterDesktopMode_DWFlagDisabled_deviceEligible_configDevOptionOn_returnsFalse() { + setDeviceEligibleForDesktopMode(true) doReturn(true).whenever(mockResources).getBoolean( eq(R.bool.config_isDesktopModeDevOptionSupported) ) @@ -98,7 +102,7 @@ class DesktopModeStatusTest : ShellTestCase() { Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION ) @Test - fun canEnterDesktopMode_DWFlagDisabled_configDevOptionOn_returnsFalse() { + fun canEnterDesktopMode_DWFlagDisabled_deviceNotEligible_configDevOptionOn_returnsFalse() { doReturn(true).whenever(mockResources).getBoolean( eq(R.bool.config_isDesktopModeDevOptionSupported) ) @@ -111,7 +115,7 @@ class DesktopModeStatusTest : ShellTestCase() { Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION ) @Test - fun canEnterDesktopMode_DWFlagDisabled_configDevOptionOn_flagOverrideOn_returnsTrue() { + fun canEnterDesktopMode_DWFlagDisabled_deviceNotEligible_forceUsingDevOption_returnsTrue() { doReturn(true).whenever(mockResources).getBoolean( eq(R.bool.config_isDesktopModeDevOptionSupported) ) @@ -123,14 +127,7 @@ class DesktopModeStatusTest : ShellTestCase() { @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) @Test - fun canEnterDesktopMode_DWFlagEnabled_configsOff_returnsFalse() { - assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isFalse() - } - - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - @Test - fun canEnterDesktopMode_DWFlagEnabled_configDesktopModeOff_returnsFalse() { + fun canEnterDesktopMode_DWFlagEnabled_deviceNotEligible_returnsFalse() { doReturn(true).whenever(mockResources).getBoolean( eq(R.bool.config_isDesktopModeDevOptionSupported) ) @@ -141,17 +138,8 @@ class DesktopModeStatusTest : ShellTestCase() { @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) @Test - fun canEnterDesktopMode_DWFlagEnabled_configDesktopModeOn_returnsTrue() { - doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)) - - assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isTrue() - } - - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - @Test - fun canEnterDesktopMode_DWFlagEnabled_configsOff_disableDeviceRestrictions_returnsTrue() { - disableEnforceDeviceRestriction() + fun canEnterDesktopMode_DWFlagEnabled_deviceEligible_returnsTrue() { + setDeviceEligibleForDesktopMode(true) assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isTrue() } @@ -159,7 +147,7 @@ class DesktopModeStatusTest : ShellTestCase() { @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) @Test - fun canEnterDesktopMode_DWFlagEnabled_configDevOptionOn_flagOverrideOn_returnsTrue() { + fun canEnterDesktopMode_DWFlagEnabled_deviceNotEligible_forceUsingDevOption_returnsTrue() { doReturn(true).whenever(mockResources).getBoolean( eq(R.bool.config_isDesktopModeDevOptionSupported) ) @@ -198,6 +186,28 @@ class DesktopModeStatusTest : ShellTestCase() { assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isTrue() } + @DisableFlags(Flags.FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION) + @Test + fun canShowDesktopExperienceDevOption_flagDisabled_returnsFalse() { + setDeviceEligibleForDesktopMode(true) + + assertThat(DesktopModeStatus.canShowDesktopExperienceDevOption(mockContext)).isFalse() + } + + @EnableFlags(Flags.FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION) + @Test + fun canShowDesktopExperienceDevOption_flagEnabled_deviceNotEligible_returnsFalse() { + assertThat(DesktopModeStatus.canShowDesktopExperienceDevOption(mockContext)).isFalse() + } + + @EnableFlags(Flags.FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION) + @Test + fun canShowDesktopExperienceDevOption_flagEnabled_deviceEligible_returnsTrue() { + setDeviceEligibleForDesktopMode(true) + + assertThat(DesktopModeStatus.canShowDesktopExperienceDevOption(mockContext)).isTrue() + } + private fun resetEnforceDeviceRestriction() { setEnforceDeviceRestriction(true) } @@ -232,4 +242,10 @@ class DesktopModeStatusTest : ShellTestCase() { DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, override.setting ) } + + private fun setDeviceEligibleForDesktopMode(eligible: Boolean) { + val deviceRestrictions = DesktopModeStatus::class.java.getDeclaredField("ENFORCE_DEVICE_RESTRICTIONS") + deviceRestrictions.isAccessible = true + deviceRestrictions.setBoolean(/* obj= */ null, /* z= */ !eligible) + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt index 8b4cf6d1fabe..e40d97c68554 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt @@ -67,7 +67,7 @@ class DesktopModeWindowDecorViewModelAppHandleOnlyTest : .spyStatic(DesktopModeStatus::class.java) .spyStatic(DragPositioningCallbackUtility::class.java) .startMocking() - doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + doReturn(false).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) } doReturn(true).`when` { DesktopModeStatus.overridesShowAppHandle(any())} setUpCommon() } 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 737780ed8024..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 @@ -116,7 +116,7 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest .spyStatic(DragPositioningCallbackUtility::class.java) .startMocking() - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(Mockito.any()) } + doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(Mockito.any()) } doReturn(false).`when` { DesktopModeStatus.overridesShowAppHandle(Mockito.any()) } setUpCommon() @@ -379,37 +379,21 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - fun testWindowDecor_desktopModeUnsupportedOnDevice_deviceRestrictionsOverridden_decorCreated() { - // Simulate enforce device restrictions system property overridden to false - whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(false) - // Simulate device that doesn't support desktop mode - doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - - val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN) - setUpMockDecorationsForTasks(task) - - onTaskOpening(task) - assertTrue(windowDecorByTaskIdSpy.contains(task.taskId)) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - fun testWindowDecor_deviceSupportsDesktopMode_decorCreated() { + fun testWindowDecor_deviceEligibleForDesktopMode_decorCreated() { // Simulate default enforce device restrictions system property whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true) val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN) - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) } setUpMockDecorationsForTasks(task) onTaskOpening(task) - assertTrue(windowDecorByTaskIdSpy.contains(task.taskId)) + assertTrue(task.taskId in windowDecorByTaskIdSpy) } @Test fun testOnDecorMaximizedOrRestored_togglesTaskSize_maximize() { - val maxOrRestoreListenerCaptor = forClass(Function0::class.java) - as ArgumentCaptor<Function0<Unit>> + val maxOrRestoreListenerCaptor = forClass(Function0::class.java as Class<Function0<Unit>>) val decor = createOpenTaskDecoration( windowingMode = WINDOWING_MODE_FREEFORM, onMaxOrRestoreListenerCaptor = maxOrRestoreListenerCaptor @@ -655,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), @@ -893,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/libs/androidfw/Android.bp b/libs/androidfw/Android.bp index 12b1dd794a03..3dc53c5051e9 100644 --- a/libs/androidfw/Android.bp +++ b/libs/androidfw/Android.bp @@ -288,6 +288,7 @@ cc_benchmark { "tests/AttributeResolution_bench.cpp", "tests/CursorWindow_bench.cpp", "tests/Generic_bench.cpp", + "tests/LocaleDataLookup_bench.cpp", "tests/SparseEntry_bench.cpp", "tests/Theme_bench.cpp", ], diff --git a/libs/androidfw/LocaleDataLookup.cpp b/libs/androidfw/LocaleDataLookup.cpp index 6e751a77f355..ea9e9a2d4280 100644 --- a/libs/androidfw/LocaleDataLookup.cpp +++ b/libs/androidfw/LocaleDataLookup.cpp @@ -7518,6 +7518,13 @@ const char* lookupLikelyScript(uint32_t packed_lang_region) { } } +/* + * TODO: Consider turning the below switch statement into binary search + * to save the disk space when the table is larger in the future. + * Disassembled code shows that the jump table emitted by clang can be + * 4x larger than the data in disk size, but it depends on the optimization option. + * However, a switch statement will benefit from the future of compiler improvement. + */ bool isLocaleRepresentative(uint32_t language_and_region, const char* script) { const uint64_t packed_locale = ((static_cast<uint64_t>(language_and_region)) << 32u) | diff --git a/libs/androidfw/tests/LocaleDataLookup_bench.cpp b/libs/androidfw/tests/LocaleDataLookup_bench.cpp new file mode 100644 index 000000000000..60ce3b944551 --- /dev/null +++ b/libs/androidfw/tests/LocaleDataLookup_bench.cpp @@ -0,0 +1,57 @@ +/* + * 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. + */ + +#include "benchmark/benchmark.h" + +#include "androidfw/LocaleDataLookup.h" + +namespace android { + +static void BM_LocaleDataLookupIsLocaleRepresentative(benchmark::State& state) { + for (auto&& _ : state) { + isLocaleRepresentative(packLocale("en", "US"), "Latn"); + isLocaleRepresentative(packLocale("es", "ES"), "Latn"); + isLocaleRepresentative(packLocale("zh", "CN"), "Hans"); + isLocaleRepresentative(packLocale("pt", "BR"), "Latn"); + isLocaleRepresentative(packLocale("ar", "EG"), "Arab"); + isLocaleRepresentative(packLocale("hi", "IN"), "Deva"); + isLocaleRepresentative(packLocale("jp", "JP"), "Jpan"); + } +} +BENCHMARK(BM_LocaleDataLookupIsLocaleRepresentative); + +static void BM_LocaleDataLookupLikelyScript(benchmark::State& state) { + for (auto&& _ : state) { + lookupLikelyScript(packLocale("en", "")); + lookupLikelyScript(packLocale("es", "")); + lookupLikelyScript(packLocale("zh", "")); + lookupLikelyScript(packLocale("pt", "")); + lookupLikelyScript(packLocale("ar", "")); + lookupLikelyScript(packLocale("hi", "")); + lookupLikelyScript(packLocale("jp", "")); + lookupLikelyScript(packLocale("en", "US")); + lookupLikelyScript(packLocale("es", "ES")); + lookupLikelyScript(packLocale("zh", "CN")); + lookupLikelyScript(packLocale("pt", "BR")); + lookupLikelyScript(packLocale("ar", "EG")); + lookupLikelyScript(packLocale("hi", "IN")); + lookupLikelyScript(packLocale("jp", "JP")); + } +} +BENCHMARK(BM_LocaleDataLookupLikelyScript); + + +} // namespace android 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/nfc-non-updatable/java/android/nfc/cardemulation/ApduServiceInfo.java b/nfc-non-updatable/java/android/nfc/cardemulation/ApduServiceInfo.java index 93d6eb73dcae..e83b9f1afddb 100644 --- a/nfc-non-updatable/java/android/nfc/cardemulation/ApduServiceInfo.java +++ b/nfc-non-updatable/java/android/nfc/cardemulation/ApduServiceInfo.java @@ -572,8 +572,10 @@ public final class ApduServiceInfo implements Parcelable { if (mAutoTransact.getOrDefault(plf.toUpperCase(Locale.ROOT), false)) { return true; } - List<Pattern> patternMatches = mAutoTransactPatterns.keySet().stream() - .filter(p -> p.matcher(plf).matches()).toList(); + boolean isPattern = plf.contains("?") || plf.contains("*"); + List<Pattern> patternMatches = mAutoTransactPatterns.keySet().stream().filter( + p -> isPattern ? p.toString().equals(plf) : p.matcher(plf).matches()).toList(); + if (patternMatches == null || patternMatches.size() == 0) { return false; } 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 0a7d880677d8..744388f47d0e 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -314,6 +314,7 @@ filegroup { "tests/src/**/systemui/statusbar/policy/WalletControllerImplTest.kt", "tests/src/**/keyguard/ClockEventControllerTest.kt", "tests/src/**/systemui/bluetooth/qsdialog/BluetoothStateInteractorTest.kt", + "tests/src/**/systemui/bluetooth/qsdialog/BluetoothDetailsContentManagerTest.kt", "tests/src/**/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt", "tests/src/**/systemui/bluetooth/qsdialog/BluetoothTileDialogRepositoryTest.kt", "tests/src/**/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt", @@ -424,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/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 6c96279711d0..ac53dcb1982a 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -2013,3 +2013,10 @@ flag { description: "Show a Locked by your watch indicator on the keyguard when the device is locked by the watch." bug: "387322459" } + +flag { + name: "decouple_view_controller_in_animlib" + namespace: "systemui" + description: "Decouple view and controller in AnimLib." + bug: "393241010" +} 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/animation/ExpandableController.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt index 103a9b5cf5f4..c5d2802c8941 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt @@ -319,7 +319,7 @@ internal class ExpandableControllerImpl( override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { delegate.onTransitionAnimationStart(isExpandingFullyAbove) - overlay.value = composeViewRoot.rootView.overlay as ViewGroupOverlay + overlay.value = transitionContainer.overlay as ViewGroupOverlay cujType?.let { InteractionJankMonitor.getInstance().begin(composeViewRoot, it) } } 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/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt index d3417022565b..fa5e8aceef1d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt @@ -109,15 +109,22 @@ constructor( } if (isShadeLayoutWide && !isBypassEnabled) { with(notificationSection) { - Notifications( - areNotificationsVisible = areNotificationsVisible, - isShadeLayoutWide = true, - burnInParams = null, - modifier = - Modifier.fillMaxWidth(0.5f) - .fillMaxHeight() - .align(alignment = Alignment.TopEnd), - ) + Box(modifier = Modifier.fillMaxHeight()) { + AodPromotedNotificationArea( + modifier = + Modifier.fillMaxWidth(0.5f) + .align(alignment = Alignment.TopStart) + ) + Notifications( + areNotificationsVisible = areNotificationsVisible, + isShadeLayoutWide = true, + burnInParams = null, + modifier = + Modifier.fillMaxWidth(0.5f) + .fillMaxHeight() + .align(alignment = Alignment.TopEnd), + ) + } } } } @@ -142,9 +149,18 @@ constructor( } } else { Column { - AodPromotedNotificationArea() + if (!isShadeLayoutWide) { + AodPromotedNotificationArea() + } AodNotificationIcons( - modifier = Modifier.padding(start = aodIconPadding) + modifier = + Modifier.padding( + top = + dimensionResource( + R.dimen.keyguard_status_view_bottom_margin + ), + start = aodIconPadding, + ) ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt index 931134795a53..26e7524f4fa8 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt @@ -29,7 +29,6 @@ import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult -import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn @@ -44,10 +43,7 @@ import com.android.systemui.scene.ui.composable.Overlay import com.android.systemui.shade.ui.composable.OverlayShade import com.android.systemui.shade.ui.composable.OverlayShadeHeader import com.android.systemui.shade.ui.composable.SingleShadeMeasurePolicy -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView -import com.android.systemui.statusbar.phone.ui.StatusBarIconController -import com.android.systemui.statusbar.phone.ui.TintedIconManager import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -58,11 +54,6 @@ class NotificationsShadeOverlay constructor( private val actionsViewModelFactory: NotificationsShadeOverlayActionsViewModel.Factory, private val contentViewModelFactory: NotificationsShadeOverlayContentViewModel.Factory, - private val tintedIconManagerFactory: TintedIconManager.Factory, - private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory, - private val statusBarIconController: StatusBarIconController, - private val notificationIconContainerStatusBarViewBinder: - NotificationIconContainerStatusBarViewBinder, private val shadeSession: SaveableSession, private val stackScrollView: Lazy<NotificationScrollView>, private val clockSection: DefaultClockSection, @@ -94,18 +85,16 @@ constructor( } OverlayShade( - isShadeLayoutWide = viewModel.isShadeLayoutWide, panelAlignment = Alignment.TopStart, modifier = modifier, onScrimClicked = viewModel::onScrimClicked, header = { + val headerViewModel = + rememberViewModel("NotificationsShadeOverlayHeader") { + viewModel.shadeHeaderViewModelFactory.create() + } OverlayShadeHeader( - viewModelFactory = viewModel.shadeHeaderViewModelFactory, - createTintedIconManager = tintedIconManagerFactory::create, - createBatteryMeterViewController = batteryMeterViewControllerFactory::create, - statusBarIconController = statusBarIconController, - notificationIconContainerStatusBarViewBinder = - notificationIconContainerStatusBarViewBinder, + viewModel = headerViewModel, modifier = Modifier.element(NotificationsShade.Elements.StatusBar) .layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader), @@ -114,7 +103,7 @@ constructor( ) { Box { Column { - if (viewModel.showHeader) { + if (viewModel.showClock) { val burnIn = rememberBurnIn(clockInteractor) with(clockSection) { @@ -140,8 +129,7 @@ constructor( modifier = Modifier.fillMaxWidth(), ) } - // Communicates the bottom position of the drawable area within the shade to - // NSSL. + // Communicates the bottom position of the drawable area within the shade to NSSL. NotificationStackCutoffGuideline( stackScrollView = stackScrollView.get(), viewModel = placeholderViewModel, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt index 4bfbb3a908fa..62a8cc5a7fe3 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt @@ -16,7 +16,6 @@ package com.android.systemui.qs.ui.composable -import android.view.ViewGroup import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateDpAsState @@ -76,7 +75,6 @@ import com.android.compose.animation.scene.animateSceneFloatAsState import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.modifiers.thenIf import com.android.compose.windowsizeclass.LocalWindowSizeClass -import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout import com.android.systemui.compose.modifiers.sysuiResTag @@ -100,15 +98,14 @@ import com.android.systemui.res.R import com.android.systemui.scene.session.ui.composable.SaveableSession import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.Scene +import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorViewModel import com.android.systemui.shade.ui.composable.CollapsedShadeHeader import com.android.systemui.shade.ui.composable.ExpandedShadeHeader import com.android.systemui.shade.ui.composable.Shade import com.android.systemui.shade.ui.composable.ShadeHeader +import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel -import com.android.systemui.statusbar.phone.StatusBarLocation -import com.android.systemui.statusbar.phone.ui.StatusBarIconController -import com.android.systemui.statusbar.phone.ui.TintedIconManager import dagger.Lazy import javax.inject.Inject import javax.inject.Named @@ -125,9 +122,6 @@ constructor( private val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory, private val actionsViewModelFactory: QuickSettingsUserActionsViewModel.Factory, private val contentViewModelFactory: QuickSettingsSceneContentViewModel.Factory, - private val tintedIconManagerFactory: TintedIconManager.Factory, - private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory, - private val statusBarIconController: StatusBarIconController, private val mediaCarouselController: MediaCarouselController, @Named(MediaModule.QS_PANEL) private val mediaHost: MediaHost, ) : ExclusiveActivatable(), Scene { @@ -145,16 +139,26 @@ constructor( @Composable override fun ContentScope.Content(modifier: Modifier) { + val viewModel = + rememberViewModel("QuickSettingsScene-viewModel") { contentViewModelFactory.create() } + val headerViewModel = + rememberViewModel("QuickSettingsScene-headerViewModel") { + viewModel.shadeHeaderViewModelFactory.create() + } + val brightnessMirrorViewModel = + rememberViewModel("QuickSettingsScene-brightnessMirrorViewModel") { + viewModel.brightnessMirrorViewModelFactory.create() + } + val notificationsPlaceholderViewModel = + rememberViewModel("QuickSettingsScene-notifPlaceholderViewModel") { + notificationsPlaceholderViewModelFactory.create() + } QuickSettingsScene( notificationStackScrollView = notificationStackScrollView.get(), - viewModelFactory = contentViewModelFactory, - notificationsPlaceholderViewModel = - rememberViewModel("QuickSettingsScene-notifPlaceholderViewModel") { - notificationsPlaceholderViewModelFactory.create() - }, - createTintedIconManager = tintedIconManagerFactory::create, - createBatteryMeterViewController = batteryMeterViewControllerFactory::create, - statusBarIconController = statusBarIconController, + viewModel = viewModel, + headerViewModel = headerViewModel, + brightnessMirrorViewModel = brightnessMirrorViewModel, + notificationsPlaceholderViewModel = notificationsPlaceholderViewModel, mediaCarouselController = mediaCarouselController, mediaHost = mediaHost, modifier = modifier, @@ -166,23 +170,16 @@ constructor( @Composable private fun ContentScope.QuickSettingsScene( notificationStackScrollView: NotificationScrollView, - viewModelFactory: QuickSettingsSceneContentViewModel.Factory, + viewModel: QuickSettingsSceneContentViewModel, + headerViewModel: ShadeHeaderViewModel, + brightnessMirrorViewModel: BrightnessMirrorViewModel, notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel, - createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, - createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, - statusBarIconController: StatusBarIconController, mediaCarouselController: MediaCarouselController, mediaHost: MediaHost, modifier: Modifier = Modifier, shadeSession: SaveableSession, ) { val cutoutLocation = LocalDisplayCutout.current.location - - val viewModel = rememberViewModel("QuickSettingsScene-viewModel") { viewModelFactory.create() } - val brightnessMirrorViewModel = - rememberViewModel("QuickSettingsScene-brightnessMirrorViewModel") { - viewModel.brightnessMirrorViewModelFactory.create() - } val brightnessMirrorShowing by brightnessMirrorViewModel.isShowing.collectAsStateWithLifecycle() val contentAlpha by animateFloatAsState( @@ -222,8 +219,7 @@ private fun ContentScope.QuickSettingsScene( .graphicsLayer { alpha = contentAlpha } .thenIf(shouldPunchHoleBehindScrim) { // Render the scene to an offscreen buffer so that BlendMode.DstOut only clears - // this - // scene (and not the one under it) during a scene transition. + // this scene (and not the one under it) during a scene transition. Modifier.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) } .thenIf(cutoutLocation != CutoutLocation.CENTER) { Modifier.displayCutoutPadding() } @@ -348,21 +344,11 @@ private fun ContentScope.QuickSettingsScene( fadeOut(tween(customizingAnimationDuration)), ) { ExpandedShadeHeader( - viewModelFactory = viewModel.shadeHeaderViewModelFactory, - createTintedIconManager = createTintedIconManager, - createBatteryMeterViewController = - createBatteryMeterViewController, - statusBarIconController = statusBarIconController, + viewModel = headerViewModel, modifier = Modifier.padding(horizontal = 16.dp), ) } - else -> - CollapsedShadeHeader( - viewModelFactory = viewModel.shadeHeaderViewModelFactory, - createTintedIconManager = createTintedIconManager, - createBatteryMeterViewController = createBatteryMeterViewController, - statusBarIconController = statusBarIconController, - ) + else -> CollapsedShadeHeader(viewModel = headerViewModel) } Spacer(modifier = Modifier.height(16.dp)) // This view has its own horizontal padding diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt index 3ec14a23421c..2fa370458ab0 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt @@ -45,7 +45,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult -import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.dagger.SysUISingleton @@ -67,13 +66,10 @@ import com.android.systemui.shade.ui.composable.OverlayShade import com.android.systemui.shade.ui.composable.OverlayShadeHeader import com.android.systemui.shade.ui.composable.QuickSettingsOverlayHeader import com.android.systemui.shade.ui.composable.SingleShadeMeasurePolicy -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel -import com.android.systemui.statusbar.phone.ui.StatusBarIconController -import com.android.systemui.statusbar.phone.ui.TintedIconManager import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -84,11 +80,7 @@ class QuickSettingsShadeOverlay constructor( private val actionsViewModelFactory: QuickSettingsShadeOverlayActionsViewModel.Factory, private val contentViewModelFactory: QuickSettingsShadeOverlayContentViewModel.Factory, - private val tintedIconManagerFactory: TintedIconManager.Factory, - private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory, - private val statusBarIconController: StatusBarIconController, - private val notificationIconContainerStatusBarViewBinder: - NotificationIconContainerStatusBarViewBinder, + private val quickSettingsContainerViewModelFactory: QuickSettingsContainerViewModel.Factory, private val notificationStackScrollView: Lazy<NotificationScrollView>, private val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory, ) : Overlay { @@ -107,35 +99,35 @@ constructor( @Composable override fun ContentScope.Content(modifier: Modifier) { - val viewModel = - rememberViewModel("QuickSettingsShadeOverlay") { contentViewModelFactory.create() } + val contentViewModel = + rememberViewModel("QuickSettingsShadeOverlayContent") { + contentViewModelFactory.create() + } + val quickSettingsContainerViewModel = + rememberViewModel("QuickSettingsShadeOverlayContainer") { + // TODO(b/393054014): Add support for brightness mirroring. + quickSettingsContainerViewModelFactory.create(supportsBrightnessMirroring = false) + } val panelCornerRadius = with(LocalDensity.current) { OverlayShade.Dimensions.PanelCornerRadius.toPx().toInt() } - // set the bounds to null when the QuickSettings overlay disappears - DisposableEffect(Unit) { onDispose { viewModel.onPanelShapeChanged(null) } } + // Set the bounds to null when the QuickSettings overlay disappears. + DisposableEffect(Unit) { onDispose { contentViewModel.onPanelShapeChanged(null) } } Box(modifier = modifier) { SnoozeableHeadsUpNotificationSpace( stackScrollView = notificationStackScrollView.get(), viewModel = - rememberViewModel("QuickSettingsShadeOverlay") { + rememberViewModel("QuickSettingsShadeOverlayPlaceholder") { notificationsPlaceholderViewModelFactory.create() }, ) OverlayShade( - isShadeLayoutWide = viewModel.isShadeLayoutWide, panelAlignment = Alignment.TopEnd, - onScrimClicked = viewModel::onScrimClicked, + onScrimClicked = contentViewModel::onScrimClicked, header = { OverlayShadeHeader( - viewModelFactory = viewModel.shadeHeaderViewModelFactory, - createTintedIconManager = tintedIconManagerFactory::create, - createBatteryMeterViewController = - batteryMeterViewControllerFactory::create, - statusBarIconController = statusBarIconController, - notificationIconContainerStatusBarViewBinder = - notificationIconContainerStatusBarViewBinder, + viewModel = quickSettingsContainerViewModel.shadeHeaderViewModel, modifier = Modifier.element(NotificationsShade.Elements.StatusBar) .layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader), @@ -143,7 +135,7 @@ constructor( }, ) { ShadeBody( - viewModel = viewModel.quickSettingsContainerViewModel, + viewModel = quickSettingsContainerViewModel, modifier = Modifier.onPlaced { coordinates -> val boundsInWindow = coordinates.boundsInWindow() @@ -160,14 +152,12 @@ constructor( topRadius = 0, bottomRadius = panelCornerRadius, ) - viewModel.onPanelShapeChanged(shape) + contentViewModel.onPanelShapeChanged(shape) }, header = { - if (viewModel.isShadeLayoutWide) { + if (quickSettingsContainerViewModel.showHeader) { QuickSettingsOverlayHeader( - viewModelFactory = viewModel.shadeHeaderViewModelFactory, - createBatteryMeterViewController = - batteryMeterViewControllerFactory::create, + viewModel = quickSettingsContainerViewModel.shadeHeaderViewModel, modifier = Modifier.padding(top = QuickSettingsShade.Dimensions.Padding), ) 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/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt index fc59d40ec443..3d2d7c37ce48 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt @@ -58,29 +58,31 @@ import com.android.systemui.res.R /** Renders a lightweight shade UI container, as an overlay. */ @Composable fun ContentScope.OverlayShade( - isShadeLayoutWide: Boolean, panelAlignment: Alignment, onScrimClicked: () -> Unit, modifier: Modifier = Modifier, header: @Composable () -> Unit, content: @Composable () -> Unit, ) { + val isFullWidth = isFullWidthShade() Box(modifier) { Scrim(onClicked = onScrimClicked) - Box(modifier = Modifier.fillMaxSize().panelPadding(), contentAlignment = panelAlignment) { + Box( + modifier = Modifier.fillMaxSize().panelContainerPadding(isFullWidth), + contentAlignment = panelAlignment, + ) { Panel( - isShadeLayoutWide = isShadeLayoutWide, modifier = Modifier.overscroll(verticalOverscrollEffect) .element(OverlayShade.Elements.Panel) - .panelSize(), - header = header, + .panelWidth(isFullWidth), + header = header.takeIf { isFullWidth }, content = content, ) } - if (isShadeLayoutWide) { + if (!isFullWidth) { header() } } @@ -100,9 +102,8 @@ private fun ContentScope.Scrim(onClicked: () -> Unit, modifier: Modifier = Modif @Composable private fun ContentScope.Panel( - isShadeLayoutWide: Boolean, modifier: Modifier = Modifier, - header: @Composable () -> Unit, + header: (@Composable () -> Unit)?, content: @Composable () -> Unit, ) { Box(modifier = modifier.clip(OverlayShade.Shapes.RoundedCornerPanel)) { @@ -117,9 +118,7 @@ private fun ContentScope.Panel( ) Column { - if (!isShadeLayoutWide) { - header() - } + header?.invoke() // This content is intentionally rendered as a separate element from the background in // order to allow for more flexibility when defining transitions. @@ -129,14 +128,12 @@ private fun ContentScope.Panel( } @Composable -private fun Modifier.panelSize(): Modifier { - return this.then( - if (isFullWidthShade()) { - Modifier.fillMaxWidth() - } else { - Modifier.width(dimensionResource(id = R.dimen.shade_panel_width)) - } - ) +private fun Modifier.panelWidth(isFullWidthPanel: Boolean): Modifier { + return if (isFullWidthPanel) { + fillMaxWidth() + } else { + width(dimensionResource(id = R.dimen.shade_panel_width)) + } } @Composable @@ -146,27 +143,23 @@ internal fun isFullWidthShade(): Boolean { } @Composable -private fun Modifier.panelPadding(): Modifier { - val widthSizeClass = LocalWindowSizeClass.current.widthSizeClass +private fun Modifier.panelContainerPadding(isFullWidthPanel: Boolean): Modifier { + if (isFullWidthPanel) { + return this + } val systemBars = WindowInsets.systemBarsIgnoringVisibility val displayCutout = WindowInsets.displayCutout val waterfall = WindowInsets.waterfall val horizontalPadding = PaddingValues(horizontal = dimensionResource(id = R.dimen.shade_panel_margin_horizontal)) - - val combinedPadding = + return padding( combinePaddings( systemBars.asPaddingValues(), displayCutout.asPaddingValues(), waterfall.asPaddingValues(), horizontalPadding, ) - - return if (widthSizeClass == WindowWidthSizeClass.Compact) { - padding(bottom = combinedPadding.calculateBottomPadding()) - } else { - padding(combinedPadding) - } + ) } /** Creates a union of [paddingValues] by using the max padding of each edge. */ diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt index c5d28adce601..02de78bc84ce 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt @@ -74,23 +74,20 @@ import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius import com.android.systemui.compose.modifiers.sysuiResTag -import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.privacy.OngoingPrivacyChip import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.shade.ui.composable.ShadeHeader.Colors.chipBackground import com.android.systemui.shade.ui.composable.ShadeHeader.Colors.chipHighlighted import com.android.systemui.shade.ui.composable.ShadeHeader.Colors.onScrimDim import com.android.systemui.shade.ui.composable.ShadeHeader.Dimensions.CollapsedHeight import com.android.systemui.shade.ui.composable.ShadeHeader.Values.ClockScale import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel +import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel.HeaderChipHighlight import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder import com.android.systemui.statusbar.phone.NotificationIconContainer import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.phone.StatusIconContainer -import com.android.systemui.statusbar.phone.ui.StatusBarIconController -import com.android.systemui.statusbar.phone.ui.TintedIconManager import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel import com.android.systemui.statusbar.policy.Clock @@ -137,14 +134,9 @@ object ShadeHeader { /** The status bar that appears above the Shade scene on small screens */ @Composable fun ContentScope.CollapsedShadeHeader( - viewModelFactory: ShadeHeaderViewModel.Factory, - createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, - createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, - statusBarIconController: StatusBarIconController, + viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifier, ) { - val viewModel = rememberViewModel("CollapsedShadeHeader") { viewModelFactory.create() } - val cutoutLocation = LocalDisplayCutout.current.location val horizontalPadding = max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding) @@ -157,12 +149,14 @@ fun ContentScope.CollapsedShadeHeader( } } + val longerDateText by viewModel.longerDateText.collectAsStateWithLifecycle() + val shorterDateText by viewModel.shorterDateText.collectAsStateWithLifecycle() + val isShadeLayoutWide = viewModel.isShadeLayoutWide val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle() - // This layout assumes it is globally positioned at (0, 0) and is the - // same size as the screen. + // This layout assumes it is globally positioned at (0, 0) and is the same size as the screen. CutoutAwareShadeHeader( modifier = modifier.sysuiResTag(ShadeHeader.TestTags.Root), startContent = { @@ -171,9 +165,11 @@ fun ContentScope.CollapsedShadeHeader( horizontalArrangement = Arrangement.spacedBy(5.dp), modifier = Modifier.padding(horizontal = horizontalPadding), ) { - Clock(scale = 1f, viewModel = viewModel) + Clock(scale = 1f, onClick = viewModel::onClockClicked) VariableDayDate( - viewModel = viewModel, + longerDateText = longerDateText, + shorterDateText = shorterDateText, + chipHighlight = viewModel.notificationsChipHighlight, modifier = Modifier.element(ShadeHeader.Elements.CollapsedContentStart), ) } @@ -202,17 +198,17 @@ fun ContentScope.CollapsedShadeHeader( if (isShadeLayoutWide) { ShadeCarrierGroup(viewModel = viewModel) } - SystemIconChip(viewModel = viewModel, isClickable = isShadeLayoutWide) { + SystemIconChip( + onClick = viewModel::onSystemIconChipClicked.takeIf { isShadeLayoutWide } + ) { StatusIcons( viewModel = viewModel, - createTintedIconManager = createTintedIconManager, - statusBarIconController = statusBarIconController, useExpandedFormat = useExpandedTextFormat, modifier = Modifier.padding(end = 6.dp).weight(1f, fill = false), ) BatteryIcon( - viewModel = viewModel, - createBatteryMeterViewController = createBatteryMeterViewController, + createBatteryMeterViewController = + viewModel.createBatteryMeterViewController, useExpandedFormat = useExpandedTextFormat, modifier = Modifier.padding(vertical = 8.dp), ) @@ -226,18 +222,15 @@ fun ContentScope.CollapsedShadeHeader( /** The status bar that appears above the Quick Settings scene on small screens */ @Composable fun ContentScope.ExpandedShadeHeader( - viewModelFactory: ShadeHeaderViewModel.Factory, - createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, - createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, - statusBarIconController: StatusBarIconController, + viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifier, ) { - val viewModel = rememberViewModel("ExpandedShadeHeader") { viewModelFactory.create() } - val useExpandedFormat by remember { derivedStateOf { shouldUseExpandedFormat(layoutState.transitionState) } } + val longerDateText by viewModel.longerDateText.collectAsStateWithLifecycle() + val shorterDateText by viewModel.shorterDateText.collectAsStateWithLifecycle() val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle() Box(modifier = modifier.sysuiResTag(ShadeHeader.TestTags.Root)) { @@ -256,7 +249,7 @@ fun ContentScope.ExpandedShadeHeader( Box { Clock( scale = 2.57f, - viewModel = viewModel, + onClick = viewModel::onClockClicked, modifier = Modifier.align(Alignment.CenterStart), ) } @@ -275,20 +268,23 @@ fun ContentScope.ExpandedShadeHeader( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.element(ShadeHeader.Elements.ExpandedContent), ) { - VariableDayDate(viewModel = viewModel, modifier = Modifier.widthIn(max = 90.dp)) + VariableDayDate( + longerDateText = longerDateText, + shorterDateText = shorterDateText, + chipHighlight = viewModel.notificationsChipHighlight, + modifier = Modifier.widthIn(max = 90.dp), + ) Spacer(modifier = Modifier.weight(1f)) - SystemIconChip(viewModel = viewModel) { + SystemIconChip { StatusIcons( viewModel = viewModel, - createTintedIconManager = createTintedIconManager, - statusBarIconController = statusBarIconController, useExpandedFormat = useExpandedFormat, modifier = Modifier.padding(end = 6.dp).weight(1f, fill = false), ) BatteryIcon( - viewModel = viewModel, useExpandedFormat = useExpandedFormat, - createBatteryMeterViewController = createBatteryMeterViewController, + createBatteryMeterViewController = + viewModel.createBatteryMeterViewController, ) } } @@ -302,15 +298,9 @@ fun ContentScope.ExpandedShadeHeader( */ @Composable fun ContentScope.OverlayShadeHeader( - viewModelFactory: ShadeHeaderViewModel.Factory, - createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, - createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, - statusBarIconController: StatusBarIconController, - notificationIconContainerStatusBarViewBinder: NotificationIconContainerStatusBarViewBinder, + viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifier, ) { - val viewModel = rememberViewModel("OverlayShadeHeader") { viewModelFactory.create() } - val horizontalPadding = max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding) @@ -318,8 +308,7 @@ fun ContentScope.OverlayShadeHeader( val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle() - // This layout assumes it is globally positioned at (0, 0) and is the - // same size as the screen. + // This layout assumes it is globally positioned at (0, 0) and is the same size as the screen. CutoutAwareShadeHeader( modifier = modifier.sysuiResTag(ShadeHeader.TestTags.Root), startContent = { @@ -330,21 +319,32 @@ fun ContentScope.OverlayShadeHeader( if (isShadeLayoutWide) { Clock( scale = 1f, - viewModel = viewModel, + onClick = viewModel::onClockClicked, modifier = Modifier.padding(horizontal = 4.dp), ) Spacer(modifier = Modifier.width(5.dp)) } - NotificationIconChip(viewModel = viewModel) { + val chipHighlight = viewModel.notificationsChipHighlight + NotificationIconChip( + chipHighlight = chipHighlight, + onClick = viewModel::onNotificationIconChipClicked, + ) { if (isShadeLayoutWide) { NotificationIcons( - viewModel = viewModel, + chipHighlight = chipHighlight, notificationIconContainerStatusBarViewBinder = - notificationIconContainerStatusBarViewBinder, + viewModel.notificationIconContainerStatusBarViewBinder, modifier = Modifier.width(IntrinsicSize.Min).height(20.dp), ) } else { - VariableDayDate(viewModel = viewModel) + val longerDateText by viewModel.longerDateText.collectAsStateWithLifecycle() + val shorterDateText by + viewModel.shorterDateText.collectAsStateWithLifecycle() + VariableDayDate( + longerDateText = longerDateText, + shorterDateText = shorterDateText, + chipHighlight = viewModel.notificationsChipHighlight, + ) } } } @@ -355,20 +355,22 @@ fun ContentScope.OverlayShadeHeader( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = horizontalPadding), ) { - SystemIconChip(viewModel = viewModel, isClickable = true, showBackground = true) { + val chipHighlight = viewModel.quickSettingsChipHighlight + SystemIconChip( + chipHighlight = chipHighlight, + onClick = viewModel::onSystemIconChipClicked, + ) { StatusIcons( viewModel = viewModel, - createTintedIconManager = createTintedIconManager, - statusBarIconController = statusBarIconController, useExpandedFormat = false, - highlightable = true, modifier = Modifier.padding(end = 6.dp).weight(1f, fill = false), + chipHighlight = chipHighlight, ) BatteryIcon( - viewModel = viewModel, - createBatteryMeterViewController = createBatteryMeterViewController, + createBatteryMeterViewController = + viewModel.createBatteryMeterViewController, useExpandedFormat = false, - highlightable = true, + chipHighlight = chipHighlight, ) } if (isPrivacyChipVisible) { @@ -391,13 +393,7 @@ fun ContentScope.OverlayShadeHeader( /** The header that appears at the top of the Quick Settings shade overlay. */ @Composable -fun ContentScope.QuickSettingsOverlayHeader( - viewModelFactory: ShadeHeaderViewModel.Factory, - createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, - modifier: Modifier = Modifier, -) { - val viewModel = rememberViewModel("QuickSettingsOverlayHeader") { viewModelFactory.create() } - +fun QuickSettingsOverlayHeader(viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifier) { Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, @@ -405,8 +401,7 @@ fun ContentScope.QuickSettingsOverlayHeader( ) { ShadeCarrierGroup(viewModel = viewModel) BatteryIcon( - viewModel = viewModel, - createBatteryMeterViewController = createBatteryMeterViewController, + createBatteryMeterViewController = viewModel.createBatteryMeterViewController, useExpandedFormat = true, ) } @@ -468,11 +463,7 @@ private fun CutoutAwareShadeHeader( } @Composable -private fun ContentScope.Clock( - scale: Float, - viewModel: ShadeHeaderViewModel, - modifier: Modifier = Modifier, -) { +private fun ContentScope.Clock(scale: Float, onClick: () -> Unit, modifier: Modifier = Modifier) { val layoutDirection = LocalLayoutDirection.current Element(key = ShadeHeader.Elements.Clock, modifier = modifier) { @@ -500,18 +491,17 @@ private fun ContentScope.Clock( 0.5f, ) } - .clickable { viewModel.onClockClicked() }, + .clickable { onClick() }, ) } } @Composable private fun BatteryIcon( - viewModel: ShadeHeaderViewModel, createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, useExpandedFormat: Boolean, - highlightable: Boolean = false, modifier: Modifier = Modifier, + chipHighlight: HeaderChipHighlight = HeaderChipHighlight.None, ) { val localContext = LocalContext.current val themedContext = @@ -521,8 +511,6 @@ private fun BatteryIcon( val inverseColor = Utils.getColorAttrDefaultColor(themedContext, android.R.attr.textColorPrimaryInverse) - val isHighlighted = viewModel.highlightQuickSettingsIcons - AndroidView( factory = { context -> val batteryIcon = BatteryMeterView(context, null) @@ -544,18 +532,12 @@ private fun BatteryIcon( // TODO(b/298525212): use MODE_ESTIMATE in collapsed view when the screen // has no center cutout. See [QsBatteryModeController.getBatteryMode] batteryIcon.setPercentShowMode( - if (useExpandedFormat) { - BatteryMeterView.MODE_ESTIMATE - } else { - BatteryMeterView.MODE_ON - } + if (useExpandedFormat) BatteryMeterView.MODE_ESTIMATE else BatteryMeterView.MODE_ON ) - if (highlightable) { - if (isHighlighted) { - batteryIcon.updateColors(primaryColor, inverseColor, inverseColor) - } else { - batteryIcon.updateColors(primaryColor, inverseColor, primaryColor) - } + if (chipHighlight is HeaderChipHighlight.Strong) { + batteryIcon.updateColors(primaryColor, inverseColor, inverseColor) + } else if (chipHighlight is HeaderChipHighlight.Weak) { + batteryIcon.updateColors(primaryColor, inverseColor, primaryColor) } }, modifier = modifier, @@ -590,14 +572,12 @@ private fun ShadeCarrierGroup(viewModel: ShadeHeaderViewModel, modifier: Modifie @Composable private fun NotificationIcons( - viewModel: ShadeHeaderViewModel, + chipHighlight: HeaderChipHighlight, notificationIconContainerStatusBarViewBinder: NotificationIconContainerStatusBarViewBinder, modifier: Modifier = Modifier, ) { val scope = rememberCoroutineScope() - val isHighlighted = viewModel.highlightNotificationIcons - AndroidView( factory = { context -> NotificationIconContainer(context, null).also { view -> @@ -610,7 +590,7 @@ private fun NotificationIcons( } } }, - update = { it.setUseInverseOverrideIconColor(isHighlighted) }, + update = { it.setUseInverseOverrideIconColor(chipHighlight is HeaderChipHighlight.Strong) }, modifier = modifier, ) } @@ -618,11 +598,9 @@ private fun NotificationIcons( @Composable private fun ContentScope.StatusIcons( viewModel: ShadeHeaderViewModel, - createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, - statusBarIconController: StatusBarIconController, useExpandedFormat: Boolean, - highlightable: Boolean = false, modifier: Modifier = Modifier, + chipHighlight: HeaderChipHighlight = HeaderChipHighlight.None, ) { val localContext = LocalContext.current val themedContext = @@ -632,8 +610,6 @@ private fun ContentScope.StatusIcons( val inverseColor = Utils.getColorAttrDefaultColor(themedContext, android.R.attr.textColorPrimaryInverse) - val isHighlighted = viewModel.highlightQuickSettingsIcons - val carrierIconSlots = listOf(stringResource(id = com.android.internal.R.string.status_bar_mobile)) val cameraSlot = stringResource(id = com.android.internal.R.string.status_bar_camera) @@ -648,12 +624,14 @@ private fun ContentScope.StatusIcons( viewModel.isLocationIndicationEnabled.collectAsStateWithLifecycle() val iconContainer = remember { StatusIconContainer(themedContext, null) } - val iconManager = remember { createTintedIconManager(iconContainer, StatusBarLocation.QS) } + val iconManager = remember { + viewModel.createTintedIconManager(iconContainer, StatusBarLocation.QS) + } AndroidView( factory = { context -> iconManager.setTint(primaryColor, inverseColor) - statusBarIconController.addIconGroup(iconManager) + viewModel.statusBarIconController.addIconGroup(iconManager) iconContainer }, @@ -686,12 +664,10 @@ private fun ContentScope.StatusIcons( iconContainer.removeIgnoredSlot(locationSlot) } - if (highlightable) { - if (isHighlighted) { - iconManager.setTint(inverseColor, primaryColor) - } else { - iconManager.setTint(primaryColor, inverseColor) - } + if (chipHighlight is HeaderChipHighlight.Strong) { + iconManager.setTint(inverseColor, primaryColor) + } else if (chipHighlight is HeaderChipHighlight.Weak) { + iconManager.setTint(primaryColor, inverseColor) } }, modifier = modifier, @@ -700,15 +676,12 @@ private fun ContentScope.StatusIcons( @Composable private fun NotificationIconChip( - viewModel: ShadeHeaderViewModel, + chipHighlight: HeaderChipHighlight, + onClick: () -> Unit, modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit, ) { val interactionSource = remember { MutableInteractionSource() } - val backgroundColor = - if (viewModel.highlightNotificationIcons) MaterialTheme.colorScheme.chipHighlighted - else MaterialTheme.colorScheme.chipBackground - Box(modifier = modifier) { Row( modifier = @@ -716,16 +689,15 @@ private fun NotificationIconChip( .clickable( interactionSource = interactionSource, indication = null, - onClick = { viewModel.onNotificationIconChipClicked() }, + onClick = { onClick() }, ) - .thenIf(DualShade.isEnabled) { - Modifier.graphicsLayer { - shape = RoundedCornerShape(25.dp) - clip = true - } - .background(backgroundColor) - .padding(horizontal = 8.dp, vertical = 4.dp) - } + .clip(RoundedCornerShape(25.dp)) + .background( + if (chipHighlight is HeaderChipHighlight.Strong) + MaterialTheme.colorScheme.chipHighlighted + else MaterialTheme.colorScheme.chipBackground + ) + .padding(horizontal = 8.dp, vertical = 4.dp) ) { content() } @@ -734,10 +706,9 @@ private fun NotificationIconChip( @Composable private fun SystemIconChip( - viewModel: ShadeHeaderViewModel, - isClickable: Boolean = false, - showBackground: Boolean = false, modifier: Modifier = Modifier, + chipHighlight: HeaderChipHighlight = HeaderChipHighlight.None, + onClick: (() -> Unit)? = null, content: @Composable RowScope.() -> Unit, ) { val interactionSource = remember { MutableInteractionSource() } @@ -746,14 +717,14 @@ private fun SystemIconChip( Modifier.clip(RoundedCornerShape(CollapsedHeight / 4)) .background(MaterialTheme.colorScheme.onScrimDim) val backgroundColor = - if (viewModel.highlightQuickSettingsIcons) MaterialTheme.colorScheme.chipHighlighted + if (chipHighlight is HeaderChipHighlight.Strong) MaterialTheme.colorScheme.chipHighlighted else MaterialTheme.colorScheme.chipBackground Row( verticalAlignment = Alignment.CenterVertically, modifier = modifier - .thenIf(showBackground) { + .thenIf(chipHighlight !is HeaderChipHighlight.None) { Modifier.graphicsLayer { shape = RoundedCornerShape(25.dp) clip = true @@ -761,11 +732,11 @@ private fun SystemIconChip( .background(backgroundColor) .padding(horizontal = 8.dp, vertical = 4.dp) } - .thenIf(isClickable) { + .thenIf(onClick != null) { Modifier.clickable( interactionSource = interactionSource, indication = null, - onClick = { viewModel.onSystemIconChipClicked() }, + onClick = { onClick?.invoke() }, ) } .thenIf(isHovered) { hoverModifier }, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index f829a0d6facf..5040490da8f6 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -101,6 +101,7 @@ import com.android.systemui.scene.session.ui.composable.SaveableSession import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.Scene import com.android.systemui.shade.shared.model.ShadeMode +import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import com.android.systemui.shade.ui.viewmodel.ShadeSceneContentViewModel import com.android.systemui.shade.ui.viewmodel.ShadeUserActionsViewModel import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView @@ -124,8 +125,6 @@ object Shade { object Dimensions { val HorizontalPadding = 16.dp - val ScrimOverscrollLimit = 32.dp - const val ScrimVisibilityThreshold = 5f } } @@ -160,15 +159,22 @@ constructor( override val userActions: Flow<Map<UserAction, UserActionResult>> = actionsViewModel.actions @Composable - override fun ContentScope.Content(modifier: Modifier) = + override fun ContentScope.Content(modifier: Modifier) { + val viewModel = + rememberViewModel("ShadeScene-viewModel") { contentViewModelFactory.create() } + val headerViewModel = + rememberViewModel("ShadeScene-headerViewModel") { + viewModel.shadeHeaderViewModelFactory.create() + } + val notificationsPlaceholderViewModel = + rememberViewModel("ShadeScene-notifPlaceholderViewModel") { + notificationsPlaceholderViewModelFactory.create() + } ShadeScene( notificationStackScrollView.get(), - viewModel = - rememberViewModel("ShadeScene-viewModel") { contentViewModelFactory.create() }, - notificationsPlaceholderViewModel = - rememberViewModel("ShadeScene-notifPlaceholderViewModel") { - notificationsPlaceholderViewModelFactory.create() - }, + viewModel = viewModel, + headerViewModel = headerViewModel, + notificationsPlaceholderViewModel = notificationsPlaceholderViewModel, createTintedIconManager = tintedIconManagerFactory::create, createBatteryMeterViewController = batteryMeterViewControllerFactory::create, statusBarIconController = statusBarIconController, @@ -180,6 +186,7 @@ constructor( usingCollapsedLandscapeMedia = Utils.useCollapsedMediaInLandscape(LocalContext.current.resources), ) + } init { qqsMediaHost.expansion = EXPANDED @@ -196,6 +203,7 @@ constructor( private fun ContentScope.ShadeScene( notificationStackScrollView: NotificationScrollView, viewModel: ShadeSceneContentViewModel, + headerViewModel: ShadeHeaderViewModel, notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel, createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, @@ -207,13 +215,13 @@ private fun ContentScope.ShadeScene( shadeSession: SaveableSession, usingCollapsedLandscapeMedia: Boolean, ) { - val shadeMode by viewModel.shadeMode.collectAsStateWithLifecycle() when (shadeMode) { is ShadeMode.Single -> SingleShade( notificationStackScrollView = notificationStackScrollView, viewModel = viewModel, + headerViewModel = headerViewModel, notificationsPlaceholderViewModel = notificationsPlaceholderViewModel, createTintedIconManager = createTintedIconManager, createBatteryMeterViewController = createBatteryMeterViewController, @@ -228,10 +236,8 @@ private fun ContentScope.ShadeScene( SplitShade( notificationStackScrollView = notificationStackScrollView, viewModel = viewModel, + headerViewModel = headerViewModel, notificationsPlaceholderViewModel = notificationsPlaceholderViewModel, - createTintedIconManager = createTintedIconManager, - createBatteryMeterViewController = createBatteryMeterViewController, - statusBarIconController = statusBarIconController, mediaCarouselController = mediaCarouselController, mediaHost = qsMediaHost, modifier = modifier, @@ -245,6 +251,7 @@ private fun ContentScope.ShadeScene( private fun ContentScope.SingleShade( notificationStackScrollView: NotificationScrollView, viewModel: ShadeSceneContentViewModel, + headerViewModel: ShadeHeaderViewModel, notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel, createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, @@ -332,10 +339,7 @@ private fun ContentScope.SingleShade( }, content = { CollapsedShadeHeader( - viewModelFactory = viewModel.shadeHeaderViewModelFactory, - createTintedIconManager = createTintedIconManager, - createBatteryMeterViewController = createBatteryMeterViewController, - statusBarIconController = statusBarIconController, + viewModel = headerViewModel, modifier = Modifier.layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader), ) @@ -413,10 +417,8 @@ private fun ContentScope.SingleShade( private fun ContentScope.SplitShade( notificationStackScrollView: NotificationScrollView, viewModel: ShadeSceneContentViewModel, + headerViewModel: ShadeHeaderViewModel, notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel, - createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, - createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, - statusBarIconController: StatusBarIconController, mediaCarouselController: MediaCarouselController, mediaHost: MediaHost, modifier: Modifier = Modifier, @@ -509,10 +511,7 @@ private fun ContentScope.SplitShade( Column(modifier = Modifier.fillMaxSize()) { CollapsedShadeHeader( - viewModelFactory = viewModel.shadeHeaderViewModelFactory, - createTintedIconManager = createTintedIconManager, - createBatteryMeterViewController = createBatteryMeterViewController, - statusBarIconController = statusBarIconController, + viewModel = headerViewModel, modifier = Modifier.then(brightnessMirrorShowingModifier) .padding(horizontal = { unfoldTranslationXForStartSide.roundToInt() }), diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/VariableDayDate.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/VariableDayDate.kt index 93eca86e15cf..64aada52626b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/VariableDayDate.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/VariableDayDate.kt @@ -5,17 +5,19 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.Layout -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.theme.colorAttr -import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel +import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel.HeaderChipHighlight @Composable -fun VariableDayDate(viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifier) { - val longerText = viewModel.longerDateText.collectAsStateWithLifecycle() - val shorterText = viewModel.shorterDateText.collectAsStateWithLifecycle() - +fun VariableDayDate( + longerDateText: String, + shorterDateText: String, + chipHighlight: HeaderChipHighlight, + modifier: Modifier = Modifier, +) { val textColor = - if (viewModel.highlightNotificationIcons) colorAttr(android.R.attr.textColorPrimaryInverse) + if (chipHighlight is HeaderChipHighlight.Strong) + colorAttr(android.R.attr.textColorPrimaryInverse) else colorAttr(android.R.attr.textColorPrimary) Layout( @@ -23,7 +25,7 @@ fun VariableDayDate(viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifi listOf( { Text( - text = longerText.value, + text = longerDateText, style = MaterialTheme.typography.bodyMedium, color = textColor, maxLines = 1, @@ -31,7 +33,7 @@ fun VariableDayDate(viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifi }, { Text( - text = shorterText.value, + text = shorterDateText, style = MaterialTheme.typography.bodyMedium, color = textColor, maxLines = 1, 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/complication/DreamClockTimeComplicationTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/DreamClockTimeComplicationTest.java index 22ab4994f026..92ccf1294554 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/DreamClockTimeComplicationTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/DreamClockTimeComplicationTest.java @@ -23,7 +23,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; -import android.view.View; +import android.widget.TextClock; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -41,6 +41,8 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.Locale; + @SmallTest @RunWith(AndroidJUnit4.class) public class DreamClockTimeComplicationTest extends SysuiTestCase { @@ -68,7 +70,7 @@ public class DreamClockTimeComplicationTest extends SysuiTestCase { private ComplicationViewModel mComplicationViewModel; @Mock - private View mView; + private TextClock mView; @Mock private ComplicationLayoutParams mLayoutParams; @@ -86,6 +88,7 @@ public class DreamClockTimeComplicationTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); when(mComponentFactory.create()).thenReturn(mComponent); when(mComponent.getViewHolder()).thenReturn(mDreamClockTimeViewHolder); + when(mView.getTextLocale()).thenReturn(Locale.US); mMonitor = SelfExecutingMonitor.createInstance(); } 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/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt index 675960832edc..43db50ad675f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt @@ -124,35 +124,35 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() { } @Test - fun showHeader_showsOnNarrowScreen() = + fun showClock_showsOnNarrowScreen() = testScope.runTest { kosmos.shadeRepository.setShadeLayoutWide(false) // Shown when notifications are present. kosmos.activeNotificationListRepository.setActiveNotifs(1) runCurrent() - assertThat(underTest.showHeader).isTrue() + assertThat(underTest.showClock).isTrue() // Hidden when notifications are not present. kosmos.activeNotificationListRepository.setActiveNotifs(0) runCurrent() - assertThat(underTest.showHeader).isFalse() + assertThat(underTest.showClock).isFalse() } @Test - fun showHeader_hidesOnWideScreen() = + fun showClock_hidesOnWideScreen() = testScope.runTest { kosmos.shadeRepository.setShadeLayoutWide(true) // Hidden when notifications are present. kosmos.activeNotificationListRepository.setActiveNotifs(1) runCurrent() - assertThat(underTest.showHeader).isFalse() + assertThat(underTest.showClock).isFalse() // Hidden when notifications are not present. kosmos.activeNotificationListRepository.setActiveNotifs(0) runCurrent() - assertThat(underTest.showHeader).isFalse() + assertThat(underTest.showClock).isFalse() } private fun TestScope.lockDevice() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelTest.kt new file mode 100644 index 000000000000..6e26fa119888 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelTest.kt @@ -0,0 +1,82 @@ +/* + * 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.qs.ui.viewmodel + +import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn +import com.android.systemui.qs.composefragment.dagger.usingMediaInComposeFragment +import com.android.systemui.scene.domain.startable.sceneContainerStartable +import com.android.systemui.shade.domain.interactor.enableDualShade +import com.android.systemui.shade.domain.interactor.shadeModeInteractor +import com.android.systemui.testKosmos +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 + +@SmallTest +@RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper +@EnableSceneContainer +class QuickSettingsContainerViewModelTest : SysuiTestCase() { + + private val kosmos = + testKosmos().apply { + usingMediaInComposeFragment = false // This is not for the compose fragment + } + private val testScope = kosmos.testScope + + private val shadeModeInteractor = kosmos.shadeModeInteractor + + private val underTest by lazy { + kosmos.quickSettingsContainerViewModelFactory.create(supportsBrightnessMirroring = false) + } + + @Before + fun setUp() { + kosmos.sceneContainerStartable.start() + kosmos.enableDualShade() + underTest.activateIn(testScope) + } + + @Test + fun showHeader_showsOnNarrowScreen() = + testScope.runTest { + kosmos.enableDualShade(wideLayout = false) + val isShadeLayoutWide by collectLastValue(shadeModeInteractor.isShadeLayoutWide) + assertThat(isShadeLayoutWide).isFalse() + + assertThat(underTest.showHeader).isTrue() + } + + @Test + fun showHeader_hidesOnWideScreen() = + testScope.runTest { + kosmos.enableDualShade(wideLayout = true) + val isShadeLayoutWide by collectLastValue(shadeModeInteractor.isShadeLayoutWide) + assertThat(isShadeLayoutWide).isTrue() + + assertThat(underTest.showHeader).isFalse() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt index 01714d7a4b87..b532554f5dfd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt @@ -127,24 +127,6 @@ class QuickSettingsShadeOverlayContentViewModelTest : SysuiTestCase() { } @Test - fun showHeader_showsOnNarrowScreen() = - testScope.runTest { - kosmos.enableDualShade(wideLayout = false) - runCurrent() - - assertThat(underTest.showHeader).isTrue() - } - - @Test - fun showHeader_hidesOnWideScreen() = - testScope.runTest { - kosmos.enableDualShade(wideLayout = true) - runCurrent() - - assertThat(underTest.showHeader).isFalse() - } - - @Test fun onPanelShapeChanged() = testScope.runTest { var actual: ShadeScrimShape? = null 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/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 62c360400582..85e59364d6b6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -375,7 +375,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { SystemClock systemClock = new FakeSystemClock(); mStatusBarStateController = new StatusBarStateControllerImpl( mUiEventLogger, - () -> mKosmos.getInteractionJankMonitor(), mJavaAdapter, () -> mKeyguardInteractor, () -> mKeyguardTransitionInteractor, @@ -456,7 +455,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mock(HeadsUpManager.class), new StatusBarStateControllerImpl( new UiEventLoggerFake(), - () -> mKosmos.getInteractionJankMonitor(), mJavaAdapter, () -> mKeyguardInteractor, () -> mKeyguardTransitionInteractor, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt index 8ce20d2a05e9..d08c8a7c5974 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt @@ -23,6 +23,9 @@ import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.disableDualShade import com.android.systemui.shade.domain.interactor.enableDualShade +import com.android.systemui.shade.domain.interactor.enableSingleShade +import com.android.systemui.shade.domain.interactor.enableSplitShade +import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel.HeaderChipHighlight import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.fakeMobileIconsInteractor import com.android.systemui.testKosmos @@ -273,6 +276,116 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { assertThat(currentOverlays).doesNotContain(Overlays.QuickSettingsShade) } + @Test + fun highlightChips_notifsOpenInSingleShade_bothNone() = + testScope.runTest { + kosmos.enableSingleShade() + val currentScene by collectLastValue(sceneInteractor.currentScene) + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + setScene(Scenes.Shade) + assertThat(currentScene).isEqualTo(Scenes.Shade) + assertThat(currentOverlays).isEmpty() + + assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.None) + assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.None) + } + + @Test + fun highlightChips_notifsOpenInSplitShade_bothNone() = + testScope.runTest { + kosmos.enableSplitShade() + val currentScene by collectLastValue(sceneInteractor.currentScene) + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + setScene(Scenes.Shade) + assertThat(currentScene).isEqualTo(Scenes.Shade) + assertThat(currentOverlays).isEmpty() + + assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.None) + assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.None) + } + + @Test + fun highlightChips_quickSettingsOpenInSingleShade_bothNone() = + testScope.runTest { + kosmos.enableSingleShade() + val currentScene by collectLastValue(sceneInteractor.currentScene) + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + setScene(Scenes.QuickSettings) + assertThat(currentScene).isEqualTo(Scenes.QuickSettings) + assertThat(currentOverlays).isEmpty() + + assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.None) + assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.None) + } + + @Test + fun highlightChips_notifsOpenInDualShade_notifsStrongQuickSettingsWeak() = + testScope.runTest { + kosmos.enableDualShade() + val currentScene by collectLastValue(sceneInteractor.currentScene) + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + + // Test the lockscreen scenario. + setScene(Scenes.Lockscreen) + setOverlay(Overlays.NotificationsShade) + assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Strong) + assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Weak) + + // Test the unlocked scenario. + setDeviceEntered(true) + setScene(Scenes.Gone) + setOverlay(Overlays.NotificationsShade) + assertThat(currentScene).isEqualTo(Scenes.Gone) + assertThat(currentOverlays).isNotEmpty() + assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Strong) + assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Weak) + } + + @Test + fun highlightChips_quickSettingsOpenInDualShade_notifsWeakQuickSettingsStrong() = + testScope.runTest { + kosmos.enableDualShade() + val currentScene by collectLastValue(sceneInteractor.currentScene) + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + + // Test the lockscreen scenario. + setScene(Scenes.Lockscreen) + setOverlay(Overlays.QuickSettingsShade) + assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Weak) + assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Strong) + + // Test the unlocked scenario. + setDeviceEntered(true) + setScene(Scenes.Gone) + setOverlay(Overlays.QuickSettingsShade) + assertThat(currentScene).isEqualTo(Scenes.Gone) + assertThat(currentOverlays).isNotEmpty() + assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Weak) + assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Strong) + } + + @Test + fun highlightChips_noOverlaysInDualShade_bothNone() = + testScope.runTest { + kosmos.enableDualShade() + val currentScene by collectLastValue(sceneInteractor.currentScene) + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + + // Test the lockscreen scenario. + setScene(Scenes.Lockscreen) + assertThat(currentOverlays).isEmpty() + assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.None) + assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.None) + + // Test the unlocked scenario. + setDeviceEntered(true) + setScene(Scenes.Gone) + assertThat(currentScene).isEqualTo(Scenes.Gone) + assertThat(currentOverlays).isEmpty() + assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.None) + assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.None) + } + companion object { private val SUB_1 = SubscriptionModel( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt index a51e0c0add37..a458ab6e713c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt @@ -33,7 +33,6 @@ import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteract import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.parameterizeSceneContainerFlag -import com.android.systemui.jank.interactionJankMonitor import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor @@ -113,7 +112,6 @@ class StatusBarStateControllerImplTest(flags: FlagsParameterization) : SysuiTest object : StatusBarStateControllerImpl( uiEventLogger, - { kosmos.interactionJankMonitor }, JavaAdapter(testScope.backgroundScope), { kosmos.keyguardInteractor }, { kosmos.keyguardTransitionInteractor }, 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/promoted/PromotedNotificationContentExtractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt index abb1edf8cb27..8054bd113771 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt @@ -237,9 +237,9 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { assertThat(content).isNotNull() assertThat(content?.style).isEqualTo(Style.Progress) - assertThat(content?.progress).isNotNull() - assertThat(content?.progress?.progress).isEqualTo(75) - assertThat(content?.progress?.progressMax).isEqualTo(100) + assertThat(content?.newProgress).isNotNull() + assertThat(content?.newProgress?.progress).isEqualTo(75) + assertThat(content?.newProgress?.progressMax).isEqualTo(100) } @Test @@ -255,6 +255,43 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { assertThat(content?.style).isEqualTo(Style.Ineligible) } + @Test + @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) + fun extractsContent_fromOldProgressDeterminate() { + val entry = createEntry { + setProgress(TEST_PROGRESS_MAX, TEST_PROGRESS, /* indeterminate= */ false) + } + + val content = extractContent(entry) + + assertThat(content).isNotNull() + + val oldProgress = content?.oldProgress + assertThat(oldProgress).isNotNull() + + assertThat(content).isNotNull() + assertThat(content?.oldProgress).isNotNull() + assertThat(content?.oldProgress?.progress).isEqualTo(TEST_PROGRESS) + assertThat(content?.oldProgress?.max).isEqualTo(TEST_PROGRESS_MAX) + assertThat(content?.oldProgress?.isIndeterminate).isFalse() + } + + @Test + @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) + fun extractsContent_fromOldProgressIndeterminate() { + val entry = createEntry { + setProgress(TEST_PROGRESS_MAX, TEST_PROGRESS, /* indeterminate= */ true) + } + + val content = extractContent(entry) + + assertThat(content).isNotNull() + assertThat(content?.oldProgress).isNotNull() + assertThat(content?.oldProgress?.progress).isEqualTo(TEST_PROGRESS) + assertThat(content?.oldProgress?.max).isEqualTo(TEST_PROGRESS_MAX) + assertThat(content?.oldProgress?.isIndeterminate).isTrue() + } + private fun extractContent(entry: NotificationEntry): PromotedNotificationContentModel? { val recoveredBuilder = Notification.Builder(context, entry.sbn.notification) return underTest.extractContent(entry, recoveredBuilder) @@ -277,6 +314,9 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { private const val TEST_CONTENT_TEXT = "content text" private const val TEST_SHORT_CRITICAL_TEXT = "short" + private const val TEST_PROGRESS = 50 + private const val TEST_PROGRESS_MAX = 100 + private const val TEST_PERSON_NAME = "person name" private const val TEST_PERSON_KEY = "person key" private val TEST_PERSON = 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/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 351fffdad45b..a96ebe7b4fd6 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1783,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/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt index 0303048436c9..94fca218c74f 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt @@ -53,7 +53,7 @@ constructor( private val deviceItemActionInteractorImpl: DeviceItemActionInteractorImpl, ) : DeviceItemActionInteractor { - override suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) { + override suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog?) { withContext(backgroundDispatcher) { if (!audioSharingInteractor.audioSharingAvailable()) { return@withContext deviceItemActionInteractorImpl.onClick(deviceItem, dialog) @@ -70,10 +70,18 @@ constructor( DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> { if (audioSharingInteractor.qsDialogImprovementAvailable()) { withContext(mainDispatcher) { - delegateFactory - .create(deviceItem.cachedBluetoothDevice) - .createDialog() - .let { dialogTransitionAnimator.showFromDialog(it, dialog) } + val audioSharingDialog = + delegateFactory + .create(deviceItem.cachedBluetoothDevice) + .createDialog() + + if (dialog != null) { + audioSharingDialog.let { + dialogTransitionAnimator.showFromDialog(it, dialog) + } + } else { + audioSharingDialog.show() + } } } else { launchSettings(deviceItem.cachedBluetoothDevice.device, dialog) @@ -141,7 +149,7 @@ constructor( ) } - private fun launchSettings(device: BluetoothDevice, dialog: SystemUIDialog) { + private fun launchSettings(device: BluetoothDevice, dialog: SystemUIDialog?) { val intent = Intent(Settings.ACTION_BLUETOOTH_SETTINGS).apply { putExtra( @@ -155,7 +163,8 @@ constructor( activityStarter.postStartActivityDismissingKeyguard( intent, 0, - dialogTransitionAnimator.createActivityTransitionController(dialog), + if (dialog == null) null + else dialogTransitionAnimator.createActivityTransitionController(dialog), ) } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentManager.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentManager.kt new file mode 100644 index 000000000000..0be28f3c5a97 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentManager.kt @@ -0,0 +1,442 @@ +/* + * 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.bluetooth.qsdialog + +import android.view.LayoutInflater +import android.view.View +import android.view.View.AccessibilityDelegate +import android.view.View.GONE +import android.view.View.INVISIBLE +import android.view.View.VISIBLE +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.view.accessibility.AccessibilityNodeInfo +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction +import android.widget.Button +import android.widget.ImageView +import android.widget.ProgressBar +import android.widget.Switch +import android.widget.TextView +import androidx.annotation.StringRes +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.android.internal.R as InternalR +import com.android.internal.logging.UiEventLogger +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.res.R +import com.android.systemui.util.time.SystemClock +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext + +data class DeviceItemClick(val deviceItem: DeviceItem, val clickedView: View, val target: Target) { + enum class Target { + ENTIRE_ROW, + ACTION_ICON, + } +} + +/** View content manager for showing active, connected and saved bluetooth devices. */ +class BluetoothDetailsContentManager +@AssistedInject +internal constructor( + @Assisted private val initialUiProperties: BluetoothTileDialogViewModel.UiProperties, + @Assisted private val cachedContentHeight: Int, + @Assisted private val bluetoothTileDialogCallback: BluetoothTileDialogCallback, + @Assisted private val isInDialog: Boolean, + @Assisted private val doneButtonCallback: () -> Unit, + @Main private val mainDispatcher: CoroutineDispatcher, + private val systemClock: SystemClock, + private val uiEventLogger: UiEventLogger, + private val logger: BluetoothTileDialogLogger, +) { + + private val mutableBluetoothStateToggle: MutableStateFlow<Boolean?> = MutableStateFlow(null) + internal val bluetoothStateToggle + get() = mutableBluetoothStateToggle.asStateFlow() + + private val mutableBluetoothAutoOnToggle: MutableStateFlow<Boolean?> = MutableStateFlow(null) + internal val bluetoothAutoOnToggle + get() = mutableBluetoothAutoOnToggle.asStateFlow() + + private val mutableDeviceItemClick: MutableStateFlow<DeviceItemClick?> = MutableStateFlow(null) + internal val deviceItemClick + get() = mutableDeviceItemClick.asStateFlow() + + private val mutableContentHeight: MutableStateFlow<Int?> = MutableStateFlow(null) + internal val contentHeight + get() = mutableContentHeight.asStateFlow() + + private val deviceItemAdapter: Adapter = Adapter() + + private var lastUiUpdateMs: Long = -1 + + private var lastItemRow: Int = -1 + + // UI Components + private lateinit var contentView: View + private lateinit var doneButton: Button + private lateinit var bluetoothToggle: Switch + private lateinit var subtitleTextView: TextView + private lateinit var seeAllButton: View + private lateinit var pairNewDeviceButton: View + private lateinit var deviceListView: RecyclerView + private lateinit var autoOnToggle: Switch + private lateinit var autoOnToggleLayout: View + private lateinit var autoOnToggleInfoTextView: TextView + private lateinit var audioSharingButton: Button + private lateinit var progressBarAnimation: ProgressBar + private lateinit var progressBarBackground: View + private lateinit var scrollViewContent: View + + @AssistedFactory + internal interface Factory { + fun create( + initialUiProperties: BluetoothTileDialogViewModel.UiProperties, + cachedContentHeight: Int, + dialogCallback: BluetoothTileDialogCallback, + isInDialog: Boolean, + doneButtonCallback: () -> Unit, + ): BluetoothDetailsContentManager + } + + fun bind(contentView: View) { + this.contentView = contentView + + doneButton = contentView.requireViewById(R.id.done_button) + bluetoothToggle = contentView.requireViewById(R.id.bluetooth_toggle) + subtitleTextView = contentView.requireViewById(R.id.bluetooth_tile_dialog_subtitle) + seeAllButton = contentView.requireViewById(R.id.see_all_button) + pairNewDeviceButton = contentView.requireViewById(R.id.pair_new_device_button) + deviceListView = contentView.requireViewById(R.id.device_list) + autoOnToggle = contentView.requireViewById(R.id.bluetooth_auto_on_toggle) + autoOnToggleLayout = contentView.requireViewById(R.id.bluetooth_auto_on_toggle_layout) + autoOnToggleInfoTextView = + contentView.requireViewById(R.id.bluetooth_auto_on_toggle_info_text) + audioSharingButton = contentView.requireViewById(R.id.audio_sharing_button) + progressBarAnimation = + contentView.requireViewById(R.id.bluetooth_tile_dialog_progress_animation) + progressBarBackground = + contentView.requireViewById(R.id.bluetooth_tile_dialog_progress_background) + scrollViewContent = contentView.requireViewById(R.id.scroll_view) + + setupToggle() + setupRecyclerView() + setupDoneButton() + + subtitleTextView.text = contentView.context.getString(initialUiProperties.subTitleResId) + seeAllButton.setOnClickListener { bluetoothTileDialogCallback.onSeeAllClicked(it) } + pairNewDeviceButton.setOnClickListener { + bluetoothTileDialogCallback.onPairNewDeviceClicked(it) + } + audioSharingButton.apply { + setOnClickListener { bluetoothTileDialogCallback.onAudioSharingButtonClicked(it) } + accessibilityDelegate = + object : AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfo, + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + info.addAction( + AccessibilityAction( + AccessibilityAction.ACTION_CLICK.id, + contentView.context.getString( + R.string + .quick_settings_bluetooth_audio_sharing_button_accessibility + ), + ) + ) + } + } + } + scrollViewContent.apply { + minimumHeight = + resources.getDimensionPixelSize(initialUiProperties.scrollViewMinHeightResId) + layoutParams.height = maxOf(cachedContentHeight, minimumHeight) + } + } + + fun start() { + lastUiUpdateMs = systemClock.elapsedRealtime() + } + + fun releaseView() { + mutableContentHeight.value = scrollViewContent.measuredHeight + } + + internal suspend fun animateProgressBar(animate: Boolean) { + withContext(mainDispatcher) { + if (animate) { + showProgressBar() + } else { + delay(PROGRESS_BAR_ANIMATION_DURATION_MS) + hideProgressBar() + } + } + } + + internal suspend fun onDeviceItemUpdated( + deviceItem: List<DeviceItem>, + showSeeAll: Boolean, + showPairNewDevice: Boolean, + ) { + withContext(mainDispatcher) { + val start = systemClock.elapsedRealtime() + val itemRow = deviceItem.size + showSeeAll.toInt() + showPairNewDevice.toInt() + // If not the first load, add a slight delay for smoother dialog height change + if (itemRow != lastItemRow && lastItemRow != -1) { + delay(MIN_HEIGHT_CHANGE_INTERVAL_MS - (start - lastUiUpdateMs)) + } + if (isActive) { + deviceItemAdapter.refreshDeviceItemList(deviceItem) { + seeAllButton.visibility = if (showSeeAll) VISIBLE else GONE + pairNewDeviceButton.visibility = if (showPairNewDevice) VISIBLE else GONE + // Update the height after data is updated + scrollViewContent.layoutParams.height = WRAP_CONTENT + lastUiUpdateMs = systemClock.elapsedRealtime() + lastItemRow = itemRow + logger.logDeviceUiUpdate(lastUiUpdateMs - start) + } + } + } + } + + internal fun onBluetoothStateUpdated( + isEnabled: Boolean, + uiProperties: BluetoothTileDialogViewModel.UiProperties, + ) { + bluetoothToggle.apply { + isChecked = isEnabled + setEnabled(true) + alpha = ENABLED_ALPHA + } + subtitleTextView.text = contentView.context.getString(uiProperties.subTitleResId) + autoOnToggleLayout.visibility = uiProperties.autoOnToggleVisibility + } + + internal fun onBluetoothAutoOnUpdated(isEnabled: Boolean, @StringRes infoResId: Int) { + autoOnToggle.isChecked = isEnabled + autoOnToggleInfoTextView.text = contentView.context.getString(infoResId) + } + + internal fun onAudioSharingButtonUpdated(visibility: Int, label: String?, isActive: Boolean) { + audioSharingButton.apply { + this.visibility = visibility + label?.let { text = it } + this.isActivated = isActive + } + } + + private fun setupToggle() { + bluetoothToggle.setOnCheckedChangeListener { view, isChecked -> + mutableBluetoothStateToggle.value = isChecked + view.apply { + isEnabled = false + alpha = DISABLED_ALPHA + } + logger.logBluetoothState(BluetoothStateStage.USER_TOGGLED, isChecked.toString()) + uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_TOGGLE_CLICKED) + } + + autoOnToggleLayout.visibility = initialUiProperties.autoOnToggleVisibility + autoOnToggle.setOnCheckedChangeListener { _, isChecked -> + mutableBluetoothAutoOnToggle.value = isChecked + uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_AUTO_ON_TOGGLE_CLICKED) + } + } + + private fun setupDoneButton() { + if (isInDialog) { + doneButton.setOnClickListener { doneButtonCallback() } + } else { + doneButton.visibility = GONE + } + } + + private fun setupRecyclerView() { + deviceListView.apply { + layoutManager = LinearLayoutManager(contentView.context) + adapter = deviceItemAdapter + } + } + + private fun showProgressBar() { + if (progressBarAnimation.visibility != VISIBLE) { + progressBarAnimation.visibility = VISIBLE + progressBarBackground.visibility = INVISIBLE + } + } + + private fun hideProgressBar() { + if (progressBarAnimation.visibility != INVISIBLE) { + progressBarAnimation.visibility = INVISIBLE + progressBarBackground.visibility = VISIBLE + } + } + + internal inner class Adapter : RecyclerView.Adapter<Adapter.DeviceItemViewHolder>() { + + private val diffUtilCallback = + object : DiffUtil.ItemCallback<DeviceItem>() { + override fun areItemsTheSame( + deviceItem1: DeviceItem, + deviceItem2: DeviceItem, + ): Boolean { + return deviceItem1.cachedBluetoothDevice == deviceItem2.cachedBluetoothDevice + } + + override fun areContentsTheSame( + deviceItem1: DeviceItem, + deviceItem2: DeviceItem, + ): Boolean { + return deviceItem1.type == deviceItem2.type && + deviceItem1.cachedBluetoothDevice == deviceItem2.cachedBluetoothDevice && + deviceItem1.deviceName == deviceItem2.deviceName && + deviceItem1.connectionSummary == deviceItem2.connectionSummary && + // Ignored the icon drawable + deviceItem1.iconWithDescription?.second == + deviceItem2.iconWithDescription?.second && + deviceItem1.background == deviceItem2.background && + deviceItem1.isEnabled == deviceItem2.isEnabled && + deviceItem1.actionAccessibilityLabel == deviceItem2.actionAccessibilityLabel + } + } + + private val asyncListDiffer = AsyncListDiffer(this, diffUtilCallback) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeviceItemViewHolder { + val view = + LayoutInflater.from(parent.context) + .inflate(R.layout.bluetooth_device_item, parent, false) + return DeviceItemViewHolder(view) + } + + override fun getItemCount() = asyncListDiffer.currentList.size + + override fun onBindViewHolder(holder: DeviceItemViewHolder, position: Int) { + val item = getItem(position) + holder.bind(item) + } + + internal fun getItem(position: Int) = asyncListDiffer.currentList[position] + + internal fun refreshDeviceItemList(updated: List<DeviceItem>, callback: () -> Unit) { + asyncListDiffer.submitList(updated, callback) + } + + internal inner class DeviceItemViewHolder(view: View) : RecyclerView.ViewHolder(view) { + private val container = view.requireViewById<View>(R.id.bluetooth_device_row) + private val nameView = view.requireViewById<TextView>(R.id.bluetooth_device_name) + private val summaryView = view.requireViewById<TextView>(R.id.bluetooth_device_summary) + private val iconView = view.requireViewById<ImageView>(R.id.bluetooth_device_icon) + private val actionIcon = view.requireViewById<ImageView>(R.id.gear_icon_image) + private val actionIconView = view.requireViewById<View>(R.id.gear_icon) + private val divider = view.requireViewById<View>(R.id.divider) + + internal fun bind(item: DeviceItem) { + container.apply { + isEnabled = item.isEnabled + background = item.background?.let { context.getDrawable(it) } + setOnClickListener { + mutableDeviceItemClick.value = + DeviceItemClick(item, it, DeviceItemClick.Target.ENTIRE_ROW) + uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_CLICKED) + } + + // updating icon colors + val tintColor = + context.getColor( + if (item.isActive) InternalR.color.materialColorOnPrimaryContainer + else InternalR.color.materialColorOnSurface + ) + + // update icons + iconView.apply { + item.iconWithDescription?.let { + setImageDrawable(it.first) + contentDescription = it.second + } + } + + actionIcon.setImageResource(item.actionIconRes) + actionIcon.drawable?.setTint(tintColor) + + divider.setBackgroundColor(tintColor) + + // update text styles + nameView.setTextAppearance( + if (item.isActive) R.style.TextAppearance_BluetoothTileDialog_Active + else R.style.TextAppearance_BluetoothTileDialog + ) + summaryView.setTextAppearance( + if (item.isActive) R.style.TextAppearance_BluetoothTileDialog_Active + else R.style.TextAppearance_BluetoothTileDialog + ) + + accessibilityDelegate = + object : AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfo, + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + info.addAction( + AccessibilityAction( + AccessibilityAction.ACTION_CLICK.id, + item.actionAccessibilityLabel, + ) + ) + } + } + } + nameView.text = item.deviceName + summaryView.text = item.connectionSummary + + actionIconView.setOnClickListener { + mutableDeviceItemClick.value = + DeviceItemClick(item, it, DeviceItemClick.Target.ACTION_ICON) + } + } + } + } + + internal companion object { + const val MIN_HEIGHT_CHANGE_INTERVAL_MS = 800L + const val ACTION_BLUETOOTH_DEVICE_DETAILS = + "com.android.settings.BLUETOOTH_DEVICE_DETAIL_SETTINGS" + const val ACTION_PREVIOUSLY_CONNECTED_DEVICE = + "com.android.settings.PREVIOUSLY_CONNECTED_DEVICE" + const val ACTION_PAIR_NEW_DEVICE = "android.settings.BLUETOOTH_PAIRING_SETTINGS" + const val ACTION_AUDIO_SHARING = "com.android.settings.BLUETOOTH_AUDIO_SHARING_SETTINGS" + const val DISABLED_ALPHA = 0.3f + const val ENABLED_ALPHA = 1f + const val PROGRESS_BAR_ANIMATION_DURATION_MS = 1500L + + private fun Boolean.toInt(): Int { + return if (this) 1 else 0 + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt index 56caddfbd637..3e61c45c7f25 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt @@ -18,50 +18,14 @@ package com.android.systemui.bluetooth.qsdialog import android.os.Bundle import android.view.LayoutInflater -import android.view.View -import android.view.View.AccessibilityDelegate -import android.view.View.GONE -import android.view.View.INVISIBLE -import android.view.View.VISIBLE -import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams.WRAP_CONTENT -import android.view.accessibility.AccessibilityNodeInfo -import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction -import android.widget.Button -import android.widget.ImageView -import android.widget.ProgressBar -import android.widget.Switch -import android.widget.TextView -import androidx.annotation.StringRes -import androidx.recyclerview.widget.AsyncListDiffer -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.android.internal.R as InternalR import com.android.internal.logging.UiEventLogger -import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.qs.flags.QsDetailedView import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor import com.android.systemui.statusbar.phone.SystemUIDialog -import com.android.systemui.util.time.SystemClock import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.isActive -import kotlinx.coroutines.withContext - -data class DeviceItemClick(val deviceItem: DeviceItem, val clickedView: View, val target: Target) { - enum class Target { - ENTIRE_ROW, - ACTION_ICON, - } -} /** Dialog for showing active, connected and saved bluetooth devices. */ class BluetoothTileDialogDelegate @@ -71,37 +35,13 @@ internal constructor( @Assisted private val cachedContentHeight: Int, @Assisted private val bluetoothTileDialogCallback: BluetoothTileDialogCallback, @Assisted private val dismissListener: Runnable, - @Main private val mainDispatcher: CoroutineDispatcher, - private val systemClock: SystemClock, private val uiEventLogger: UiEventLogger, - private val logger: BluetoothTileDialogLogger, private val systemuiDialogFactory: SystemUIDialog.Factory, private val shadeDialogContextInteractor: ShadeDialogContextInteractor, + private val bluetoothDetailsContentManagerFactory: BluetoothDetailsContentManager.Factory, ) : SystemUIDialog.Delegate { - private val mutableBluetoothStateToggle: MutableStateFlow<Boolean?> = MutableStateFlow(null) - internal val bluetoothStateToggle - get() = mutableBluetoothStateToggle.asStateFlow() - - private val mutableBluetoothAutoOnToggle: MutableStateFlow<Boolean?> = MutableStateFlow(null) - internal val bluetoothAutoOnToggle - get() = mutableBluetoothAutoOnToggle.asStateFlow() - - private val mutableDeviceItemClick: MutableSharedFlow<DeviceItemClick> = - MutableSharedFlow(extraBufferCapacity = 1) - internal val deviceItemClick - get() = mutableDeviceItemClick.asSharedFlow() - - private val mutableContentHeight: MutableSharedFlow<Int> = - MutableSharedFlow(extraBufferCapacity = 1) - internal val contentHeight - get() = mutableContentHeight.asSharedFlow() - - private val deviceItemAdapter: Adapter = Adapter() - - private var lastUiUpdateMs: Long = -1 - - private var lastItemRow: Int = -1 + lateinit var contentManager: BluetoothDetailsContentManager @AssistedFactory internal interface Factory { @@ -114,6 +54,9 @@ internal constructor( } override fun createDialog(): SystemUIDialog { + // If `QsDetailedView` is enabled, it should show the details view. + QsDetailedView.assertInLegacyMode() + return systemuiDialogFactory.create(this, shadeDialogContextInteractor.context) } @@ -127,362 +70,24 @@ internal constructor( dialog.setContentView(this) } - setupToggle(dialog) - setupRecyclerView(dialog) - - getSubtitleTextView(dialog).text = context.getString(initialUiProperties.subTitleResId) - dialog.requireViewById<View>(R.id.done_button).setOnClickListener { dialog.dismiss() } - getSeeAllButton(dialog).setOnClickListener { - bluetoothTileDialogCallback.onSeeAllClicked(it) - } - getPairNewDeviceButton(dialog).setOnClickListener { - bluetoothTileDialogCallback.onPairNewDeviceClicked(it) - } - getAudioSharingButtonView(dialog).apply { - setOnClickListener { bluetoothTileDialogCallback.onAudioSharingButtonClicked(it) } - accessibilityDelegate = - object : AccessibilityDelegate() { - override fun onInitializeAccessibilityNodeInfo( - host: View, - info: AccessibilityNodeInfo, - ) { - super.onInitializeAccessibilityNodeInfo(host, info) - info.addAction( - AccessibilityAction( - AccessibilityAction.ACTION_CLICK.id, - context.getString( - R.string - .quick_settings_bluetooth_audio_sharing_button_accessibility - ), - ) - ) - } - } - } - getScrollViewContent(dialog).apply { - minimumHeight = - resources.getDimensionPixelSize(initialUiProperties.scrollViewMinHeightResId) - layoutParams.height = maxOf(cachedContentHeight, minimumHeight) - } + contentManager = + bluetoothDetailsContentManagerFactory.create( + initialUiProperties, + cachedContentHeight, + bluetoothTileDialogCallback, + /* isInDialog= */ true, + /* doneButtonCallback= */ fun() { + dialog.dismiss() + }, + ) + contentManager.bind(dialog.requireViewById(R.id.root)) } override fun onStart(dialog: SystemUIDialog) { - lastUiUpdateMs = systemClock.elapsedRealtime() + contentManager.start() } override fun onStop(dialog: SystemUIDialog) { - mutableContentHeight.tryEmit(getScrollViewContent(dialog).measuredHeight) - } - - internal suspend fun animateProgressBar(dialog: SystemUIDialog, animate: Boolean) { - withContext(mainDispatcher) { - if (animate) { - showProgressBar(dialog) - } else { - delay(PROGRESS_BAR_ANIMATION_DURATION_MS) - hideProgressBar(dialog) - } - } - } - - internal suspend fun onDeviceItemUpdated( - dialog: SystemUIDialog, - deviceItem: List<DeviceItem>, - showSeeAll: Boolean, - showPairNewDevice: Boolean, - ) { - withContext(mainDispatcher) { - val start = systemClock.elapsedRealtime() - val itemRow = deviceItem.size + showSeeAll.toInt() + showPairNewDevice.toInt() - // If not the first load, add a slight delay for smoother dialog height change - if (itemRow != lastItemRow && lastItemRow != -1) { - delay(MIN_HEIGHT_CHANGE_INTERVAL_MS - (start - lastUiUpdateMs)) - } - if (isActive) { - deviceItemAdapter.refreshDeviceItemList(deviceItem) { - getSeeAllButton(dialog).visibility = if (showSeeAll) VISIBLE else GONE - getPairNewDeviceButton(dialog).visibility = - if (showPairNewDevice) VISIBLE else GONE - // Update the height after data is updated - getScrollViewContent(dialog).layoutParams.height = WRAP_CONTENT - lastUiUpdateMs = systemClock.elapsedRealtime() - lastItemRow = itemRow - logger.logDeviceUiUpdate(lastUiUpdateMs - start) - } - } - } - } - - internal fun onBluetoothStateUpdated( - dialog: SystemUIDialog, - isEnabled: Boolean, - uiProperties: BluetoothTileDialogViewModel.UiProperties, - ) { - getToggleView(dialog).apply { - isChecked = isEnabled - setEnabled(true) - alpha = ENABLED_ALPHA - } - getSubtitleTextView(dialog).text = dialog.context.getString(uiProperties.subTitleResId) - getAutoOnToggleView(dialog).visibility = uiProperties.autoOnToggleVisibility - } - - internal fun onBluetoothAutoOnUpdated( - dialog: SystemUIDialog, - isEnabled: Boolean, - @StringRes infoResId: Int, - ) { - getAutoOnToggle(dialog).isChecked = isEnabled - getAutoOnToggleInfoTextView(dialog).text = dialog.context.getString(infoResId) - } - - internal fun onAudioSharingButtonUpdated( - dialog: SystemUIDialog, - visibility: Int, - label: String?, - isActive: Boolean, - ) { - getAudioSharingButtonView(dialog).apply { - this.visibility = visibility - label?.let { text = it } - this.isActivated = isActive - } - } - - private fun setupToggle(dialog: SystemUIDialog) { - val toggleView = getToggleView(dialog) - toggleView.setOnCheckedChangeListener { view, isChecked -> - mutableBluetoothStateToggle.value = isChecked - view.apply { - isEnabled = false - alpha = DISABLED_ALPHA - } - logger.logBluetoothState(BluetoothStateStage.USER_TOGGLED, isChecked.toString()) - uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_TOGGLE_CLICKED) - } - - getAutoOnToggleView(dialog).visibility = initialUiProperties.autoOnToggleVisibility - getAutoOnToggle(dialog).setOnCheckedChangeListener { _, isChecked -> - mutableBluetoothAutoOnToggle.value = isChecked - uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_AUTO_ON_TOGGLE_CLICKED) - } - } - - private fun getToggleView(dialog: SystemUIDialog): Switch { - return dialog.requireViewById(R.id.bluetooth_toggle) - } - - private fun getSubtitleTextView(dialog: SystemUIDialog): TextView { - return dialog.requireViewById(R.id.bluetooth_tile_dialog_subtitle) - } - - private fun getSeeAllButton(dialog: SystemUIDialog): View { - return dialog.requireViewById(R.id.see_all_button) - } - - private fun getPairNewDeviceButton(dialog: SystemUIDialog): View { - return dialog.requireViewById(R.id.pair_new_device_button) - } - - private fun getDeviceListView(dialog: SystemUIDialog): RecyclerView { - return dialog.requireViewById(R.id.device_list) - } - - private fun getAutoOnToggle(dialog: SystemUIDialog): Switch { - return dialog.requireViewById(R.id.bluetooth_auto_on_toggle) - } - - private fun getAudioSharingButtonView(dialog: SystemUIDialog): Button { - return dialog.requireViewById(R.id.audio_sharing_button) - } - - private fun getAutoOnToggleView(dialog: SystemUIDialog): View { - return dialog.requireViewById(R.id.bluetooth_auto_on_toggle_layout) - } - - private fun getAutoOnToggleInfoTextView(dialog: SystemUIDialog): TextView { - return dialog.requireViewById(R.id.bluetooth_auto_on_toggle_info_text) - } - - private fun getProgressBarAnimation(dialog: SystemUIDialog): ProgressBar { - return dialog.requireViewById(R.id.bluetooth_tile_dialog_progress_animation) - } - - private fun getProgressBarBackground(dialog: SystemUIDialog): View { - return dialog.requireViewById(R.id.bluetooth_tile_dialog_progress_background) - } - - private fun getScrollViewContent(dialog: SystemUIDialog): View { - return dialog.requireViewById(R.id.scroll_view) - } - - private fun setupRecyclerView(dialog: SystemUIDialog) { - getDeviceListView(dialog).apply { - layoutManager = LinearLayoutManager(dialog.context) - adapter = deviceItemAdapter - } - } - - private fun showProgressBar(dialog: SystemUIDialog) { - val progressBarAnimation = getProgressBarAnimation(dialog) - val progressBarBackground = getProgressBarBackground(dialog) - if (progressBarAnimation.visibility != VISIBLE) { - progressBarAnimation.visibility = VISIBLE - progressBarBackground.visibility = INVISIBLE - } - } - - private fun hideProgressBar(dialog: SystemUIDialog) { - val progressBarAnimation = getProgressBarAnimation(dialog) - val progressBarBackground = getProgressBarBackground(dialog) - if (progressBarAnimation.visibility != INVISIBLE) { - progressBarAnimation.visibility = INVISIBLE - progressBarBackground.visibility = VISIBLE - } - } - - internal inner class Adapter : RecyclerView.Adapter<Adapter.DeviceItemViewHolder>() { - - private val diffUtilCallback = - object : DiffUtil.ItemCallback<DeviceItem>() { - override fun areItemsTheSame( - deviceItem1: DeviceItem, - deviceItem2: DeviceItem, - ): Boolean { - return deviceItem1.cachedBluetoothDevice == deviceItem2.cachedBluetoothDevice - } - - override fun areContentsTheSame( - deviceItem1: DeviceItem, - deviceItem2: DeviceItem, - ): Boolean { - return deviceItem1.type == deviceItem2.type && - deviceItem1.cachedBluetoothDevice == deviceItem2.cachedBluetoothDevice && - deviceItem1.deviceName == deviceItem2.deviceName && - deviceItem1.connectionSummary == deviceItem2.connectionSummary && - // Ignored the icon drawable - deviceItem1.iconWithDescription?.second == - deviceItem2.iconWithDescription?.second && - deviceItem1.background == deviceItem2.background && - deviceItem1.isEnabled == deviceItem2.isEnabled && - deviceItem1.actionAccessibilityLabel == deviceItem2.actionAccessibilityLabel - } - } - - private val asyncListDiffer = AsyncListDiffer(this, diffUtilCallback) - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeviceItemViewHolder { - val view = - LayoutInflater.from(parent.context) - .inflate(R.layout.bluetooth_device_item, parent, false) - return DeviceItemViewHolder(view) - } - - override fun getItemCount() = asyncListDiffer.currentList.size - - override fun onBindViewHolder(holder: DeviceItemViewHolder, position: Int) { - val item = getItem(position) - holder.bind(item) - } - - internal fun getItem(position: Int) = asyncListDiffer.currentList[position] - - internal fun refreshDeviceItemList(updated: List<DeviceItem>, callback: () -> Unit) { - asyncListDiffer.submitList(updated, callback) - } - - internal inner class DeviceItemViewHolder(view: View) : RecyclerView.ViewHolder(view) { - private val container = view.requireViewById<View>(R.id.bluetooth_device_row) - private val nameView = view.requireViewById<TextView>(R.id.bluetooth_device_name) - private val summaryView = view.requireViewById<TextView>(R.id.bluetooth_device_summary) - private val iconView = view.requireViewById<ImageView>(R.id.bluetooth_device_icon) - private val actionIcon = view.requireViewById<ImageView>(R.id.gear_icon_image) - private val actionIconView = view.requireViewById<View>(R.id.gear_icon) - private val divider = view.requireViewById<View>(R.id.divider) - - internal fun bind(item: DeviceItem) { - container.apply { - isEnabled = item.isEnabled - background = item.background?.let { context.getDrawable(it) } - setOnClickListener { - mutableDeviceItemClick.tryEmit( - DeviceItemClick(item, it, DeviceItemClick.Target.ENTIRE_ROW) - ) - uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_CLICKED) - } - - // updating icon colors - val tintColor = - context.getColor( - if (item.isActive) InternalR.color.materialColorOnPrimaryContainer - else InternalR.color.materialColorOnSurface - ) - - // update icons - iconView.apply { - item.iconWithDescription?.let { - setImageDrawable(it.first) - contentDescription = it.second - } - } - - actionIcon.setImageResource(item.actionIconRes) - actionIcon.drawable?.setTint(tintColor) - - divider.setBackgroundColor(tintColor) - - // update text styles - nameView.setTextAppearance( - if (item.isActive) R.style.TextAppearance_BluetoothTileDialog_Active - else R.style.TextAppearance_BluetoothTileDialog - ) - summaryView.setTextAppearance( - if (item.isActive) R.style.TextAppearance_BluetoothTileDialog_Active - else R.style.TextAppearance_BluetoothTileDialog - ) - - accessibilityDelegate = - object : AccessibilityDelegate() { - override fun onInitializeAccessibilityNodeInfo( - host: View, - info: AccessibilityNodeInfo, - ) { - super.onInitializeAccessibilityNodeInfo(host, info) - info.addAction( - AccessibilityAction( - AccessibilityAction.ACTION_CLICK.id, - item.actionAccessibilityLabel, - ) - ) - } - } - } - nameView.text = item.deviceName - summaryView.text = item.connectionSummary - - actionIconView.setOnClickListener { - mutableDeviceItemClick.tryEmit( - DeviceItemClick(item, it, DeviceItemClick.Target.ACTION_ICON) - ) - } - } - } - } - - internal companion object { - const val MIN_HEIGHT_CHANGE_INTERVAL_MS = 800L - const val ACTION_BLUETOOTH_DEVICE_DETAILS = - "com.android.settings.BLUETOOTH_DEVICE_DETAIL_SETTINGS" - const val ACTION_PREVIOUSLY_CONNECTED_DEVICE = - "com.android.settings.PREVIOUSLY_CONNECTED_DEVICE" - const val ACTION_PAIR_NEW_DEVICE = "android.settings.BLUETOOTH_PAIRING_SETTINGS" - const val ACTION_AUDIO_SHARING = "com.android.settings.BLUETOOTH_AUDIO_SHARING_SETTINGS" - const val DISABLED_ALPHA = 0.3f - const val ENABLED_ALPHA = 1f - const val PROGRESS_BAR_ANIMATION_DURATION_MS = 1500L - - private fun Boolean.toInt(): Int { - return if (this) 1 else 0 - } + contentManager.releaseView() } } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt index bf04897f6d10..9492abbeb087 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.bluetooth.qsdialog +import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.os.Bundle @@ -34,15 +35,16 @@ import com.android.systemui.Prefs import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.animation.Expandable -import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_AUDIO_SHARING -import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_PAIR_NEW_DEVICE -import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_PREVIOUSLY_CONNECTED_DEVICE +import com.android.systemui.bluetooth.qsdialog.BluetoothDetailsContentManager.Companion.ACTION_AUDIO_SHARING +import com.android.systemui.bluetooth.qsdialog.BluetoothDetailsContentManager.Companion.ACTION_PAIR_NEW_DEVICE +import com.android.systemui.bluetooth.qsdialog.BluetoothDetailsContentManager.Companion.ACTION_PREVIOUSLY_CONNECTED_DEVICE import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R +import com.android.systemui.statusbar.phone.SystemUIDialog import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -57,7 +59,12 @@ import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext -/** ViewModel for Bluetooth Dialog after clicking on the Bluetooth QS tile. */ +/** + * ViewModel for Bluetooth Dialog or Bluetooth Details View after clicking on the Bluetooth QS tile. + * + * TODO: b/378513956 Rename this class to BluetoothDetailsContentViewModel, since it's not only used + * by the dialog view. + */ @SysUISingleton internal class BluetoothTileDialogViewModel @Inject @@ -78,36 +85,61 @@ constructor( @Background private val backgroundDispatcher: CoroutineDispatcher, @Main private val sharedPreferences: SharedPreferences, private val bluetoothDialogDelegateFactory: BluetoothTileDialogDelegate.Factory, + private val bluetoothDetailsContentManagerFactory: BluetoothDetailsContentManager.Factory, ) : BluetoothTileDialogCallback { + lateinit var contentManager: BluetoothDetailsContentManager private var job: Job? = null /** - * Shows the dialog. + * Shows the details content. * - * @param view The view from which the dialog is shown. + * @param view The view from which the dialog is shown. If view is null, it should show the + * bluetooth tile details view. + * + * TODO: b/378513956 Refactor this method into 2. One is called by the dialog to show the + * dialog, another is called by the details view model to bind the view. */ - fun showDialog(expandable: Expandable?) { + fun showDetailsContent(expandable: Expandable?, view: View?) { cancelJob() job = coroutineScope.launch(context = mainDispatcher) { var updateDeviceItemJob: Job? var updateDialogUiJob: Job? = null - val dialogDelegate = createBluetoothTileDialog() - val dialog = dialogDelegate.createDialog() - val context = dialog.context - - val controller = - expandable?.dialogTransitionController( - DialogCuj( - InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, - INTERACTION_JANK_TAG, + val dialog: SystemUIDialog? + val context: Context + + if (view == null) { + // Render with dialog + val dialogDelegate = createBluetoothTileDialog() + dialog = dialogDelegate.createDialog() + context = dialog.context + + val controller = + expandable?.dialogTransitionController( + DialogCuj( + InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, + INTERACTION_JANK_TAG, + ) ) - ) - controller?.let { - dialogTransitionAnimator.show(dialog, it, animateBackgroundBoundsChange = true) - } ?: dialog.show() + controller?.let { + dialogTransitionAnimator.show( + dialog, + it, + animateBackgroundBoundsChange = true, + ) + } ?: dialog.show() + // contentManager is created after dialog.show + contentManager = dialogDelegate.contentManager + } else { + // Render with tile details view + dialog = null + context = view.context + contentManager = createContentManager() + contentManager.bind(view) + contentManager.start() + } updateDeviceItemJob = launch { deviceItemInteractor.updateDeviceItems(context, DeviceFetchTrigger.FIRST_LOAD) @@ -121,15 +153,14 @@ constructor( ) { deviceItem, showSeeAll -> updateDialogUiJob?.cancel() updateDialogUiJob = launch { - dialogDelegate.apply { + contentManager.apply { onDeviceItemUpdated( - dialog, deviceItem, showSeeAll, showPairNewDevice = bluetoothStateInteractor.isBluetoothEnabled(), ) - animateProgressBar(dialog, false) + animateProgressBar(false) } } } @@ -150,7 +181,7 @@ constructor( }, ) .onEach { - dialogDelegate.animateProgressBar(dialog, true) + contentManager.animateProgressBar(true) updateDeviceItemJob?.cancel() updateDeviceItemJob = launch { deviceItemInteractor.updateDeviceItems( @@ -171,16 +202,14 @@ constructor( .onEach { when (it) { is AudioSharingButtonState.Visible -> { - dialogDelegate.onAudioSharingButtonUpdated( - dialog, + contentManager.onAudioSharingButtonUpdated( VISIBLE, context.getString(it.resId), it.isActive, ) } is AudioSharingButtonState.Gone -> { - dialogDelegate.onAudioSharingButtonUpdated( - dialog, + contentManager.onAudioSharingButtonUpdated( GONE, label = null, isActive = false, @@ -197,8 +226,7 @@ constructor( // the device item list. bluetoothStateInteractor.bluetoothStateUpdate .onEach { - dialogDelegate.onBluetoothStateUpdated( - dialog, + contentManager.onBluetoothStateUpdated( it, UiProperties.build(it, isAutoOnToggleFeatureAvailable()), ) @@ -214,16 +242,17 @@ constructor( // bluetoothStateToggle is emitted when user toggles the bluetooth state switch, // send the new value to the bluetoothStateInteractor and animate the progress bar. - dialogDelegate.bluetoothStateToggle + contentManager.bluetoothStateToggle .filterNotNull() .onEach { - dialogDelegate.animateProgressBar(dialog, true) + contentManager.animateProgressBar(true) bluetoothStateInteractor.setBluetoothEnabled(it) } .launchIn(this) // deviceItemClick is emitted when user clicked on a device item. - dialogDelegate.deviceItemClick + contentManager.deviceItemClick + .filterNotNull() .onEach { when (it.target) { DeviceItemClick.Target.ENTIRE_ROW -> { @@ -245,7 +274,8 @@ constructor( .launchIn(this) // contentHeight is emitted when the dialog is dismissed. - dialogDelegate.contentHeight + contentManager.contentHeight + .filterNotNull() .onEach { withContext(backgroundDispatcher) { sharedPreferences.edit().putInt(CONTENT_HEIGHT_PREF_KEY, it).apply() @@ -258,8 +288,7 @@ constructor( // changed. bluetoothAutoOnInteractor.isEnabled .onEach { - dialogDelegate.onBluetoothAutoOnUpdated( - dialog, + contentManager.onBluetoothAutoOnUpdated( it, if (it) R.string.turn_on_bluetooth_auto_info_enabled else R.string.turn_on_bluetooth_auto_info_disabled, @@ -269,36 +298,48 @@ constructor( // bluetoothAutoOnToggle is emitted when user toggles the bluetooth auto on // switch, send the new value to the bluetoothAutoOnInteractor. - dialogDelegate.bluetoothAutoOnToggle + contentManager.bluetoothAutoOnToggle .filterNotNull() .onEach { bluetoothAutoOnInteractor.setEnabled(it) } .launchIn(this) } - produce<Unit> { awaitClose { dialog.cancel() } } + produce<Unit> { awaitClose { dialog?.cancel() } } } } private suspend fun createBluetoothTileDialog(): BluetoothTileDialogDelegate { - val cachedContentHeight = - withContext(backgroundDispatcher) { - sharedPreferences.getInt( - CONTENT_HEIGHT_PREF_KEY, - ViewGroup.LayoutParams.WRAP_CONTENT, - ) - } - return bluetoothDialogDelegateFactory.create( - UiProperties.build( - bluetoothStateInteractor.isBluetoothEnabled(), - isAutoOnToggleFeatureAvailable(), - ), - cachedContentHeight, + getUiProperties(), + getCachedContentHeight(), this@BluetoothTileDialogViewModel, { cancelJob() }, ) } + private suspend fun createContentManager(): BluetoothDetailsContentManager { + return bluetoothDetailsContentManagerFactory.create( + getUiProperties(), + getCachedContentHeight(), + this@BluetoothTileDialogViewModel, + /* isInDialog= */ false, + /* doneButtonCallback= */ fun() {}, + ) + } + + private suspend fun getUiProperties(): UiProperties { + return UiProperties.build( + bluetoothStateInteractor.isBluetoothEnabled(), + isAutoOnToggleFeatureAvailable(), + ) + } + + private suspend fun getCachedContentHeight(): Int { + return withContext(backgroundDispatcher) { + sharedPreferences.getInt(CONTENT_HEIGHT_PREF_KEY, ViewGroup.LayoutParams.WRAP_CONTENT) + } + } + override fun onSeeAllClicked(view: View) { uiEventLogger.log(BluetoothTileDialogUiEvent.SEE_ALL_CLICKED) startSettingsActivity(Intent(ACTION_PREVIOUSLY_CONNECTED_DEVICE), view) diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt index cb4ec37a1a66..26996ac1db39 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt @@ -27,7 +27,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext interface DeviceItemActionInteractor { - suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) + suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog?) {} suspend fun onActionIconClick(deviceItem: DeviceItem, onIntent: (Intent) -> Unit) } @@ -40,7 +40,7 @@ constructor( private val uiEventLogger: UiEventLogger, ) : DeviceItemActionInteractor { - override suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) { + override suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog?) { withContext(backgroundDispatcher) { deviceItem.cachedBluetoothDevice.apply { when (deviceItem.type) { 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/complication/DreamClockTimeComplication.java b/packages/SystemUI/src/com/android/systemui/complication/DreamClockTimeComplication.java index c709e3436cd6..8e6848a87c7d 100644 --- a/packages/SystemUI/src/com/android/systemui/complication/DreamClockTimeComplication.java +++ b/packages/SystemUI/src/com/android/systemui/complication/DreamClockTimeComplication.java @@ -16,10 +16,13 @@ package com.android.systemui.complication; +import static android.text.format.DateFormat.getBestDateTimePattern; + import static com.android.systemui.complication.dagger.DreamClockTimeComplicationComponent.DreamClockTimeComplicationModule.DREAM_CLOCK_TIME_COMPLICATION_VIEW; import static com.android.systemui.complication.dagger.RegisteredComplicationsModule.DREAM_CLOCK_TIME_COMPLICATION_LAYOUT_PARAMS; import android.view.View; +import android.widget.TextClock; import com.android.internal.logging.UiEventLogger; import com.android.systemui.CoreStartable; @@ -92,18 +95,24 @@ public class DreamClockTimeComplication implements Complication { * {@link ViewHolder} to contain value/logic associated with {@link DreamClockTimeComplication}. */ public static class DreamClockTimeViewHolder implements ViewHolder { - private final View mView; + private final TextClock mView; private final ComplicationLayoutParams mLayoutParams; @Inject DreamClockTimeViewHolder( - @Named(DREAM_CLOCK_TIME_COMPLICATION_VIEW) View view, + @Named(DREAM_CLOCK_TIME_COMPLICATION_VIEW) TextClock view, @Named(DREAM_CLOCK_TIME_COMPLICATION_LAYOUT_PARAMS) ComplicationLayoutParams layoutParams, DreamClockTimeViewController viewController) { mView = view; mLayoutParams = layoutParams; viewController.init(); + + // Support localized AM/PM marker for 12h mode in content description. + String formatSkeleton = view.is24HourModeEnabled() ? "Hm" : "hm"; + String pattern = getBestDateTimePattern(view.getTextLocale(), formatSkeleton); + view.setContentDescriptionFormat12Hour(pattern); + view.setContentDescriptionFormat24Hour(pattern); } @Override @@ -122,7 +131,7 @@ public class DreamClockTimeComplication implements Complication { @Inject DreamClockTimeViewController( - @Named(DREAM_CLOCK_TIME_COMPLICATION_VIEW) View view, + @Named(DREAM_CLOCK_TIME_COMPLICATION_VIEW) TextClock view, UiEventLogger uiEventLogger) { super(view); diff --git a/packages/SystemUI/src/com/android/systemui/complication/dagger/DreamClockTimeComplicationComponent.kt b/packages/SystemUI/src/com/android/systemui/complication/dagger/DreamClockTimeComplicationComponent.kt index 4b9ac1d58b57..9d367c91c2c3 100644 --- a/packages/SystemUI/src/com/android/systemui/complication/dagger/DreamClockTimeComplicationComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/complication/dagger/DreamClockTimeComplicationComponent.kt @@ -18,7 +18,6 @@ package com.android.systemui.complication.dagger import android.view.LayoutInflater -import android.view.View import android.widget.TextClock import com.android.internal.util.Preconditions import com.android.systemui.Flags @@ -64,7 +63,7 @@ interface DreamClockTimeComplicationComponent { @Provides @DreamClockTimeComplicationScope @Named(DREAM_CLOCK_TIME_COMPLICATION_VIEW) - fun provideComplicationView(layoutInflater: LayoutInflater): View { + fun provideComplicationView(layoutInflater: LayoutInflater): TextClock { val view = Preconditions.checkNotNull( layoutInflater.inflate( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index df3633be4625..869edfa2b886 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -29,12 +29,14 @@ import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInte import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.domain.interactor.WallpaperFocalAreaInteractor import com.android.systemui.keyguard.ui.binder.KeyguardBlueprintViewBinder +import com.android.systemui.keyguard.ui.binder.KeyguardJankBinder import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder import com.android.systemui.keyguard.ui.binder.LightRevealScrimViewBinder import com.android.systemui.keyguard.ui.view.KeyguardIndicationArea import com.android.systemui.keyguard.ui.view.KeyguardRootView import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardJankViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.keyguard.ui.viewmodel.LightRevealScrimViewModel @@ -65,6 +67,7 @@ class KeyguardViewConfigurator constructor( private val keyguardRootView: KeyguardRootView, private val keyguardRootViewModel: KeyguardRootViewModel, + private val keyguardJankViewModel: KeyguardJankViewModel, private val screenOffAnimationController: ScreenOffAnimationController, private val occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel, private val chipbarCoordinator: ChipbarCoordinator, @@ -93,9 +96,11 @@ constructor( ) : CoreStartable { private var rootViewHandle: DisposableHandle? = null + private var jankHandle: DisposableHandle? = null override fun start() { bindKeyguardRootView() + bindJankViewModel() initializeViews() if (lightRevealMigration()) { @@ -145,11 +150,9 @@ constructor( clockInteractor, wallpaperFocalAreaInteractor, keyguardClockViewModel, - interactionJankMonitor, deviceEntryHapticsInteractor, vibratorHelper, falsingManager, - keyguardViewMediator, statusBarKeyguardViewManager, mainDispatcher, msdlPlayer, @@ -157,5 +160,22 @@ constructor( ) } + private fun bindJankViewModel() { + if (SceneContainerFlag.isEnabled) { + return + } + + jankHandle?.dispose() + jankHandle = + KeyguardJankBinder.bind( + keyguardRootView, + keyguardJankViewModel, + interactionJankMonitor, + clockInteractor, + keyguardViewMediator, + mainDispatcher, + ) + } + fun getKeyguardRootView() = keyguardRootView } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 7fed7d253efe..fd50485fc3a3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -3249,7 +3249,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Log.d(TAG, "handleStartKeyguardExitAnimation startTime=" + startTime + " fadeoutDuration=" + fadeoutDuration); int currentUserId = mSelectedUserInteractor.getSelectedUserId(); - if (mGoingAwayRequestedForUserId != currentUserId) { + if (!KeyguardWmStateRefactor.isEnabled() && mGoingAwayRequestedForUserId != currentUserId) { Log.e(TAG, "Not executing handleStartKeyguardExitAnimationInner() due to userId " + "mismatch. Requested: " + mGoingAwayRequestedForUserId + ", current: " + currentUserId); @@ -3516,7 +3516,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, * app transition before finishing the current RemoteAnimation, or the keyguard being re-shown). */ private void handleCancelKeyguardExitAnimation() { - if (mGoingAwayRequestedForUserId != mSelectedUserInteractor.getSelectedUserId()) { + if (!KeyguardWmStateRefactor.isEnabled() + && mGoingAwayRequestedForUserId != mSelectedUserInteractor.getSelectedUserId()) { Log.e(TAG, "Setting pendingLock = true due to userId mismatch. Requested: " + mGoingAwayRequestedForUserId + ", current: " + mSelectedUserInteractor.getSelectedUserId()); 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/keyguard/ui/binder/KeyguardJankBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardJankBinder.kt new file mode 100644 index 000000000000..0cb684a1aabe --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardJankBinder.kt @@ -0,0 +1,96 @@ +/* + * 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.keyguard.ui.binder + +import android.view.ViewGroup +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.internal.jank.Cuj.CUJ_LOCKSCREEN_TRANSITION_FROM_AOD +import com.android.internal.jank.Cuj.CUJ_LOCKSCREEN_TRANSITION_TO_AOD +import com.android.internal.jank.Cuj.CUJ_SCREEN_OFF_SHOW_AOD +import com.android.internal.jank.InteractionJankMonitor +import com.android.systemui.keyguard.KeyguardViewMediator +import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.keyguard.ui.viewmodel.KeyguardJankViewModel +import com.android.systemui.lifecycle.repeatWhenAttached +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.ExperimentalCoroutinesApi + +/** Jank monitoring related to keyguard and transitions. */ +@OptIn(ExperimentalCoroutinesApi::class) +object KeyguardJankBinder { + @JvmStatic + fun bind( + view: ViewGroup, + viewModel: KeyguardJankViewModel, + jankMonitor: InteractionJankMonitor?, + clockInteractor: KeyguardClockInteractor, + keyguardViewMediator: KeyguardViewMediator?, + mainImmediateDispatcher: CoroutineDispatcher, + ): DisposableHandle? { + if (jankMonitor == null) { + return null + } + + fun processStep(step: TransitionStep, cuj: Int) { + val clockId = clockInteractor.renderedClockId + when (step.transitionState) { + TransitionState.STARTED -> { + val builder = + InteractionJankMonitor.Configuration.Builder.withView(cuj, view) + .setTag(clockId) + jankMonitor.begin(builder) + } + + TransitionState.CANCELED -> jankMonitor.cancel(cuj) + + TransitionState.FINISHED -> jankMonitor.end(cuj) + + TransitionState.RUNNING -> Unit + } + } + + return view.repeatWhenAttached(mainImmediateDispatcher) { + repeatOnLifecycle(Lifecycle.State.CREATED) { + launch { + viewModel.goneToAodTransition.collect { + processStep(it, CUJ_SCREEN_OFF_SHOW_AOD) + if (it.transitionState == TransitionState.FINISHED) { + keyguardViewMediator?.maybeHandlePendingLock() + } + } + } + + launch { + viewModel.lockscreenToAodTransition.collect { + processStep(it, CUJ_LOCKSCREEN_TRANSITION_TO_AOD) + } + } + + launch { + viewModel.aodToLockscreenTransition.collect { + processStep(it, CUJ_LOCKSCREEN_TRANSITION_FROM_AOD) + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index 017fe169ca88..e48af773497a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -37,8 +37,6 @@ import androidx.activity.setViewTreeOnBackPressedDispatcherOwner import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.tracing.coroutines.launchTraced as launch -import com.android.internal.jank.InteractionJankMonitor -import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD import com.android.keyguard.AuthInteractionProperties import com.android.systemui.Flags import com.android.systemui.Flags.msdlFeedback @@ -51,11 +49,9 @@ import com.android.systemui.common.ui.view.onLayoutChanged import com.android.systemui.common.ui.view.onTouchListener import com.android.systemui.customization.R as customR import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor -import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.domain.interactor.WallpaperFocalAreaInteractor import com.android.systemui.keyguard.shared.model.KeyguardState -import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.ui.view.layout.sections.AodPromotedNotificationSection import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel @@ -110,11 +106,9 @@ object KeyguardRootViewBinder { clockInteractor: KeyguardClockInteractor, wallpaperFocalAreaInteractor: WallpaperFocalAreaInteractor, clockViewModel: KeyguardClockViewModel, - interactionJankMonitor: InteractionJankMonitor?, deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor?, vibratorHelper: VibratorHelper?, falsingManager: FalsingManager?, - keyguardViewMediator: KeyguardViewMediator?, statusBarKeyguardViewManager: StatusBarKeyguardViewManager?, mainImmediateDispatcher: CoroutineDispatcher, msdlPlayer: MSDLPlayer?, @@ -308,35 +302,6 @@ object KeyguardRootViewBinder { } } - interactionJankMonitor?.let { jankMonitor -> - launch { - viewModel.goneToAodTransition.collect { - when (it.transitionState) { - TransitionState.STARTED -> { - val clockId = clockInteractor.renderedClockId - val builder = - InteractionJankMonitor.Configuration.Builder.withView( - CUJ_SCREEN_OFF_SHOW_AOD, - view, - ) - .setTag(clockId) - jankMonitor.begin(builder) - } - - TransitionState.CANCELED -> - jankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD) - - TransitionState.FINISHED -> { - keyguardViewMediator?.maybeHandlePendingLock() - jankMonitor.end(CUJ_SCREEN_OFF_SHOW_AOD) - } - - TransitionState.RUNNING -> Unit - } - } - } - } - launch { shadeInteractor.isAnyFullyExpanded.collect { isFullyAnyExpanded -> view.visibility = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodPromotedNotificationSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodPromotedNotificationSection.kt index ea4acce037b8..6c9a755148e9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodPromotedNotificationSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodPromotedNotificationSection.kt @@ -42,6 +42,10 @@ constructor( ) : KeyguardSection() { var view: ComposeView? = null + init { + logger.logSectionCreated(this) + } + override fun addViews(constraintLayout: ConstraintLayout) { if (!PromotedNotificationUiAod.isEnabled) { return @@ -56,7 +60,7 @@ constructor( constraintLayout.addView(this) } - logger.logSectionAddedViews() + logger.logSectionAddedViews(this) } override fun bindData(constraintLayout: ConstraintLayout) { @@ -68,7 +72,7 @@ constructor( // Do nothing; the binding happens in the AODPromotedNotification Composable. - logger.logSectionBoundData() + logger.logSectionBoundData(this) } override fun applyConstraints(constraintSet: ConstraintSet) { @@ -76,7 +80,8 @@ constructor( return } - checkNotNull(view) + // view may have been created by a different instance of the section (!), and we don't + // actually *need* it to set constraints, so don't check for it here. constraintSet.apply { val isShadeLayoutWide = shadeInteractor.isShadeLayoutWide.value @@ -90,7 +95,7 @@ constructor( constrainHeight(viewId, ConstraintSet.WRAP_CONTENT) } - logger.logSectionAppliedConstraints() + logger.logSectionAppliedConstraints(this) } override fun removeViews(constraintLayout: ConstraintLayout) { @@ -102,7 +107,7 @@ constructor( view = null - logger.logSectionRemovedViews() + logger.logSectionRemovedViews(this) } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardJankViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardJankViewModel.kt new file mode 100644 index 000000000000..2642505b32cc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardJankViewModel.kt @@ -0,0 +1,56 @@ +/* + * 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.keyguard.ui.viewmodel + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.Edge +import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.shared.model.KeyguardState.GONE +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.scene.shared.model.Scenes +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class KeyguardJankViewModel +@Inject +constructor( + private val keyguardInteractor: KeyguardInteractor, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, +) { + val goneToAodTransition = + keyguardTransitionInteractor.transition( + edge = Edge.create(Scenes.Gone, AOD), + edgeWithoutSceneContainer = Edge.create(GONE, AOD), + ) + + val lockscreenToAodTransition = + keyguardTransitionInteractor.transition( + edge = Edge.create(Scenes.Lockscreen, AOD), + edgeWithoutSceneContainer = Edge.create(LOCKSCREEN, AOD), + ) + + val aodToLockscreenTransition = + keyguardTransitionInteractor.transition( + edge = Edge.create(AOD, Scenes.Lockscreen), + edgeWithoutSceneContainer = Edge.create(AOD, LOCKSCREEN), + ) +} 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/controls/ui/controller/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt index c00e14c5957e..6501c437caf9 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt @@ -983,13 +983,20 @@ constructor( val overrideSize = mediaHostStatesManager.carouselSizes[location] var overridden = false overrideSize?.let { - // To be safe we're using a maximum here. The override size should always be set - // properly though. - if ( + if (SceneContainerFlag.isEnabled) { + result.measureWidth = widthInSceneContainerPx + result.measureHeight = heightInSceneContainerPx + overridden = true + } else if ( result.measureHeight != it.measuredHeight || result.measureWidth != it.measuredWidth ) { + // To be safe we're using a maximum here. The override size should always be set + // properly though. result.measureHeight = Math.max(it.measuredHeight, result.measureHeight) result.measureWidth = Math.max(it.measuredWidth, result.measureWidth) + overridden = true + } + if (overridden) { // The measureHeight and the shown height should both be set to the overridden // height result.height = result.measureHeight @@ -1001,7 +1008,6 @@ constructor( state.width = result.width } } - overridden = true } } if (overridden && state != null && state.squishFraction <= 1f) { 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/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt index 8aad61a8c7cb..c7b165415aea 100644 --- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt @@ -52,18 +52,11 @@ constructor( private val hydrator = Hydrator("NotificationsShadeOverlayContentViewModel.hydrator") - val isShadeLayoutWide: Boolean by + val showClock: Boolean by hydrator.hydratedStateOf( - traceName = "isShadeLayoutWide", - initialValue = shadeInteractor.isShadeLayoutWide.value, - source = shadeInteractor.isShadeLayoutWide, - ) - - val showHeader: Boolean by - hydrator.hydratedStateOf( - traceName = "showHeader", + traceName = "showClock", initialValue = - shouldShowHeader( + shouldShowClock( isShadeLayoutWide = shadeInteractor.isShadeLayoutWide.value, areAnyNotificationsPresent = activeNotificationsInteractor.areAnyNotificationsPresentValue, @@ -72,7 +65,7 @@ constructor( combine( shadeInteractor.isShadeLayoutWide, activeNotificationsInteractor.areAnyNotificationsPresent, - this::shouldShowHeader, + this::shouldShowClock, ), ) @@ -110,7 +103,7 @@ constructor( shadeInteractor.collapseNotificationsShade(loggingReason = "shade scrim clicked") } - private fun shouldShowHeader( + private fun shouldShowClock( isShadeLayoutWide: Boolean, areAnyNotificationsPresent: Boolean, ): Boolean { diff --git a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt index f8d442de0f55..25d53e6d1f1f 100644 --- a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt @@ -50,7 +50,7 @@ constructor( @FalsingCollectorActual private val falsingCollector: FalsingCollector, private val screenOffAnimationController: ScreenOffAnimationController, private val statusBarStateController: StatusBarStateController, - private val cameraGestureHelper: Provider<CameraGestureHelper>, + private val cameraGestureHelper: Provider<CameraGestureHelper?>, ) { /** Whether the screen is on or off. */ val isInteractive: Flow<Boolean> = repository.isInteractive @@ -154,8 +154,9 @@ constructor( // or onFinishedGoingToSleep(), carry that state forward. It will be reset by the next // onStartedGoingToSleep. val powerButtonLaunchGestureTriggered = - powerButtonLaunchGestureTriggeredOnWakeUp || - repository.wakefulness.value.powerButtonLaunchGestureTriggered + !isPowerButtonGestureSuppressed() && + (powerButtonLaunchGestureTriggeredOnWakeUp || + repository.wakefulness.value.powerButtonLaunchGestureTriggered) repository.updateWakefulness( rawState = WakefulnessState.STARTING_TO_WAKE, @@ -204,8 +205,9 @@ constructor( // If the launch gesture was previously detected via onCameraLaunchGestureDetected, carry // that state forward. It will be reset by the next onStartedGoingToSleep. val powerButtonLaunchGestureTriggered = - powerButtonLaunchGestureTriggeredDuringSleep || - repository.wakefulness.value.powerButtonLaunchGestureTriggered + !isPowerButtonGestureSuppressed() && + (powerButtonLaunchGestureTriggeredDuringSleep || + repository.wakefulness.value.powerButtonLaunchGestureTriggered) repository.updateWakefulness( rawState = WakefulnessState.ASLEEP, @@ -218,11 +220,7 @@ constructor( } fun onCameraLaunchGestureDetected() { - if ( - cameraGestureHelper - .get() - .canCameraGestureBeLaunched(statusBarStateController.getState()) - ) { + if (!isPowerButtonGestureSuppressed()) { repository.updateWakefulness(powerButtonLaunchGestureTriggered = true) } } @@ -240,6 +238,16 @@ constructor( .collect() } + /** + * Whether the power button gesture isn't allowed to launch anything even if a double tap is + * detected. + */ + private fun isPowerButtonGestureSuppressed(): Boolean { + return cameraGestureHelper + .get() + ?.canCameraGestureBeLaunched(statusBarStateController.state) == false + } + companion object { private const val FSI_WAKE_WHY = "full_screen_intent" diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt index 85b677b65aeb..cf3b4969b07d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt @@ -573,8 +573,7 @@ constructor( onDispose { qqsVisible.value = false } } val squishiness by - viewModel.containerViewModel.quickQuickSettingsViewModel.squishinessViewModel - .squishiness + viewModel.quickQuickSettingsViewModel.squishinessViewModel.squishiness .collectAsStateWithLifecycle() Column(modifier = modifier.sysuiResTag(ResIdTags.quickQsPanel)) { @@ -607,9 +606,7 @@ constructor( ) { val Tiles = @Composable { - QuickQuickSettings( - viewModel = viewModel.containerViewModel.quickQuickSettingsViewModel - ) + QuickQuickSettings(viewModel = viewModel.quickQuickSettingsViewModel) } val Media = @Composable { diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt index 219fc2fdc5ec..0dade7438720 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt @@ -59,6 +59,7 @@ import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.qs.panels.domain.interactor.TileSquishinessInteractor import com.android.systemui.qs.panels.ui.viewmodel.InFirstPageViewModel import com.android.systemui.qs.panels.ui.viewmodel.MediaInRowInLandscapeViewModel +import com.android.systemui.qs.panels.ui.viewmodel.QuickQuickSettingsViewModel import com.android.systemui.qs.ui.viewmodel.QuickSettingsContainerViewModel import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Scenes @@ -94,6 +95,7 @@ class QSFragmentComposeViewModel @AssistedInject constructor( containerViewModelFactory: QuickSettingsContainerViewModel.Factory, + quickQuickSettingsViewModelFactory: QuickQuickSettingsViewModel.Factory, @Main private val resources: Resources, footerActionsViewModelFactory: FooterActionsViewModel.Factory, private val footerActionsController: FooterActionsController, @@ -102,7 +104,7 @@ constructor( DisableFlagsInteractor: DisableFlagsInteractor, keyguardTransitionInteractor: KeyguardTransitionInteractor, private val largeScreenShadeInterpolator: LargeScreenShadeInterpolator, - private val shadeInteractor: ShadeInteractor, + shadeInteractor: ShadeInteractor, @ShadeDisplayAware configurationInteractor: ConfigurationInteractor, private val largeScreenHeaderHelper: LargeScreenHeaderHelper, private val squishinessInteractor: TileSquishinessInteractor, @@ -118,6 +120,8 @@ constructor( ) : Dumpable, ExclusiveActivatable() { val containerViewModel = containerViewModelFactory.create(true) + val quickQuickSettingsViewModel = quickQuickSettingsViewModelFactory.create() + private val qqsMediaInRowViewModel = mediaInRowInLandscapeViewModelFactory.create(LOCATION_QQS) private val qsMediaInRowViewModel = mediaInRowInLandscapeViewModelFactory.create(LOCATION_QS) @@ -475,6 +479,7 @@ constructor( } launch { hydrator.activate() } launch { containerViewModel.activate() } + launch { quickQuickSettingsViewModel.activate() } launch { qqsMediaInRowViewModel.activate() } launch { qsMediaInRowViewModel.activate() } awaitCancellation() diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt index 1f4f9f98c5b2..7701b9087e23 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt @@ -31,6 +31,7 @@ import androidx.compose.foundation.LocalOverscrollFactory import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.clipScrollableContainer import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Arrangement.spacedBy @@ -49,7 +50,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredHeightIn import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyGridScope import androidx.compose.foundation.lazy.grid.rememberLazyGridState @@ -101,7 +101,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.CustomAccessibilityAction import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.customActions -import androidx.compose.ui.semantics.onClick import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.text.style.TextAlign @@ -138,7 +137,6 @@ import com.android.systemui.qs.panels.ui.compose.selection.ResizingState import com.android.systemui.qs.panels.ui.compose.selection.ResizingState.ResizeOperation import com.android.systemui.qs.panels.ui.compose.selection.ResizingState.ResizeOperation.FinalResizeOperation import com.android.systemui.qs.panels.ui.compose.selection.ResizingState.ResizeOperation.TemporaryResizeOperation -import com.android.systemui.qs.panels.ui.compose.selection.clearSelectionTile import com.android.systemui.qs.panels.ui.compose.selection.rememberResizingState import com.android.systemui.qs.panels.ui.compose.selection.rememberSelectionState import com.android.systemui.qs.panels.ui.compose.selection.selectableTile @@ -190,6 +188,7 @@ fun DefaultEditTileGrid( columns: Int, largeTilesSpan: Int, modifier: Modifier, + onAddTile: (TileSpec) -> Unit, onRemoveTile: (TileSpec) -> Unit, onSetTiles: (List<TileSpec>) -> Unit, onResize: (TileSpec, toIcon: Boolean) -> Unit, @@ -230,20 +229,26 @@ fun DefaultEditTileGrid( modifier .fillMaxSize() // Apply top padding before the scroll so the scrollable doesn't show under - // the - // top bar + // the top bar .padding(top = innerPadding.calculateTopPadding()) .clipScrollableContainer(Orientation.Vertical) .verticalScroll(scrollState), ) { AnimatedContent( - targetState = listState.dragInProgress, - modifier = Modifier.wrapContentSize(), + targetState = listState.dragInProgress || selectionState.selected, label = "QSEditHeader", - ) { dragIsInProgress -> - EditGridHeader(Modifier.dragAndDropRemoveZone(listState, onRemoveTile)) { - if (dragIsInProgress) { - RemoveTileTarget() + ) { showRemoveTarget -> + EditGridHeader( + Modifier.dragAndDropRemoveZone(listState, onRemoveTile) + .padding(bottom = 26.dp) + ) { + if (showRemoveTarget) { + RemoveTileTarget { + selectionState.selection?.let { + selectionState.unSelect() + onRemoveTile(it.tileSpec) + } + } } else { Text(text = stringResource(id = R.string.drag_to_rearrange_tiles)) } @@ -283,7 +288,13 @@ fun DefaultEditTileGrid( Text(text = stringResource(id = R.string.drag_to_add_tiles)) } - AvailableTileGrid(otherTiles, selectionState, columns, listState) + AvailableTileGrid( + otherTiles, + selectionState, + columns, + onAddTile, + listState, + ) } } } @@ -347,22 +358,18 @@ private fun EditGridHeader( CompositionLocalProvider( LocalContentColor provides MaterialTheme.colorScheme.onBackground.copy(alpha = .5f) ) { - Box( - contentAlignment = Alignment.Center, - modifier = modifier.fillMaxWidth().wrapContentHeight(), - ) { - content() - } + Box(contentAlignment = Alignment.Center, modifier = modifier.fillMaxWidth()) { content() } } } @Composable -private fun RemoveTileTarget() { +private fun RemoveTileTarget(onClick: () -> Unit) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = tileHorizontalArrangement(), modifier = Modifier.fillMaxHeight() + .clickable(onClick = onClick) .border(1.dp, LocalContentColor.current, shape = CircleShape) .padding(10.dp), ) { @@ -441,6 +448,7 @@ private fun AvailableTileGrid( tiles: List<SizedTile<EditTileViewModel>>, selectionState: MutableSelectionState, columns: Int, + onAddTile: (TileSpec) -> Unit, dragAndDropState: DragAndDropState, ) { // Available tiles aren't visible during drag and drop, so the row/col isn't needed @@ -478,6 +486,7 @@ private fun AvailableTileGrid( index = index, dragAndDropState = dragAndDropState, selectionState = selectionState, + onAddTile = onAddTile, modifier = Modifier.weight(1f).fillMaxHeight(), ) } @@ -682,11 +691,16 @@ private fun AvailableTileGridCell( index: Int, dragAndDropState: DragAndDropState, selectionState: MutableSelectionState, + onAddTile: (TileSpec) -> Unit, modifier: Modifier = Modifier, ) { val onClickActionName = stringResource(id = R.string.accessibility_qs_edit_tile_add_action) val stateDescription = stringResource(id = R.string.accessibility_qs_edit_position, index + 1) val colors = EditModeTileDefaults.editTileColors() + val onClick = { + onAddTile(cell.tile.tileSpec) + selectionState.select(cell.tile.tileSpec, manual = false) + } // Displays the tile as an icon tile with the label underneath Column( @@ -697,11 +711,8 @@ private fun AvailableTileGridCell( Box( Modifier.fillMaxWidth() .height(TileHeight) - .clearSelectionTile(selectionState) - .semantics(mergeDescendants = true) { - onClick(onClickActionName) { false } - this.stateDescription = stateDescription - } + .clickable(onClick = onClick, onClickLabel = onClickActionName) + .semantics(mergeDescendants = true) { this.stateDescription = stateDescription } .dragAndDropTileSource( SizedTileImpl(cell.tile, cell.width), dragAndDropState, diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt index cc4c3af1dc63..1c540eed8aa0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt @@ -42,6 +42,7 @@ import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel import com.android.systemui.qs.panels.ui.viewmodel.InfiniteGridViewModel import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel +import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor.Companion.POSITION_AT_END import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.shared.ui.ElementKeys.toElementKey import com.android.systemui.res.R @@ -155,6 +156,7 @@ constructor( otherTiles = otherTiles, columns = columns, modifier = modifier, + onAddTile = { onAddTile(it, POSITION_AT_END) }, onRemoveTile = onRemoveTile, onSetTiles = onSetTiles, onResize = iconTilesViewModel::resize, diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt index c6c6dcaa896c..26dfc7224ff9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt @@ -21,6 +21,7 @@ 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.input.pointer.pointerInput import com.android.systemui.qs.pipeline.shared.TileSpec @@ -39,17 +40,19 @@ data class Selection(val tileSpec: TileSpec, val manual: Boolean) /** Holds the state of the current selection. */ class MutableSelectionState { - private var _selection = mutableStateOf<Selection?>(null) - /** The [Selection] if a tile is selected, null if not. */ - val selection by _selection + var selection by mutableStateOf<Selection?>(null) + private set + + val selected: Boolean + get() = selection != null fun select(tileSpec: TileSpec, manual: Boolean) { - _selection.value = Selection(tileSpec, manual) + selection = Selection(tileSpec, manual) } fun unSelect() { - _selection.value = null + selection = null } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java index 0109e70a467e..1cfa6632a8b0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java @@ -158,7 +158,7 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { private void handleClickEvent(@Nullable Expandable expandable) { if (mFeatureFlags.isEnabled(Flags.BLUETOOTH_QS_TILE_DIALOG)) { - mDialogViewModel.showDialog(expandable); + mDialogViewModel.showDetailsContent(expandable, /* view= */ null); } else { // Secondary clicks are header clicks, just toggle. toggleBluetooth(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt index c7db04a6b7b2..aa8e4242f64e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt @@ -19,43 +19,52 @@ package com.android.systemui.qs.ui.viewmodel import androidx.compose.runtime.getValue import com.android.systemui.brightness.ui.viewmodel.BrightnessSliderViewModel import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.lifecycle.Hydrator import com.android.systemui.qs.panels.ui.viewmodel.DetailsViewModel import com.android.systemui.qs.panels.ui.viewmodel.EditModeViewModel -import com.android.systemui.qs.panels.ui.viewmodel.QuickQuickSettingsViewModel import com.android.systemui.qs.panels.ui.viewmodel.TileGridViewModel import com.android.systemui.qs.panels.ui.viewmodel.toolbar.ToolbarViewModel +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch class QuickSettingsContainerViewModel @AssistedInject constructor( brightnessSliderViewModelFactory: BrightnessSliderViewModel.Factory, - quickQuickSettingsViewModelFactory: QuickQuickSettingsViewModel.Factory, shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory, @Assisted supportsBrightnessMirroring: Boolean, val tileGridViewModel: TileGridViewModel, val editModeViewModel: EditModeViewModel, val detailsViewModel: DetailsViewModel, val toolbarViewModelFactory: ToolbarViewModel.Factory, + shadeModeInteractor: ShadeModeInteractor, ) : ExclusiveActivatable() { + private val hydrator = Hydrator("QuickSettingsContainerViewModel.hydrator") + val brightnessSliderViewModel = brightnessSliderViewModelFactory.create(supportsBrightnessMirroring) - val quickQuickSettingsViewModel = quickQuickSettingsViewModelFactory.create() - val shadeHeaderViewModel = shadeHeaderViewModelFactory.create() + val showHeader: Boolean by + hydrator.hydratedStateOf( + traceName = "showHeader", + initialValue = !shadeModeInteractor.isShadeLayoutWide.value, + source = shadeModeInteractor.isShadeLayoutWide.map { !it }, + ) + override suspend fun onActivated(): Nothing { coroutineScope { + launch { hydrator.activate() } launch { brightnessSliderViewModel.activate() } - launch { quickQuickSettingsViewModel.activate() } launch { shadeHeaderViewModel.activate() } awaitCancellation() } diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt index d9df1ef36847..0add3f515ebf 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt @@ -16,13 +16,10 @@ package com.android.systemui.qs.ui.viewmodel -import androidx.compose.runtime.getValue import com.android.systemui.lifecycle.ExclusiveActivatable -import com.android.systemui.lifecycle.Hydrator import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.ShadeInteractor -import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape import dagger.assisted.AssistedFactory @@ -31,7 +28,6 @@ import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch /** @@ -46,39 +42,10 @@ constructor( val shadeInteractor: ShadeInteractor, val sceneInteractor: SceneInteractor, val notificationStackAppearanceInteractor: NotificationStackAppearanceInteractor, - val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory, - quickSettingsContainerViewModelFactory: QuickSettingsContainerViewModel.Factory, ) : ExclusiveActivatable() { - private val hydrator = Hydrator("QuickSettingsContainerViewModel.hydrator") - - val isShadeLayoutWide: Boolean by - hydrator.hydratedStateOf( - traceName = "isShadeLayoutWide", - initialValue = shadeInteractor.isShadeLayoutWide.value, - source = shadeInteractor.isShadeLayoutWide, - ) - - val showHeader: Boolean by - hydrator.hydratedStateOf( - traceName = "showHeader", - initialValue = !shadeInteractor.isShadeLayoutWide.value, - source = shadeInteractor.isShadeLayoutWide.map { !it }, - ) - - val quickSettingsContainerViewModel = quickSettingsContainerViewModelFactory.create(false) - - val showQuickSettingsOverlayHeader: Boolean by - hydrator.hydratedStateOf( - traceName = "showQuickSettingsOverlayHeader", - initialValue = shadeInteractor.isShadeLayoutWide.value, - source = shadeInteractor.isShadeLayoutWide, - ) - override suspend fun onActivated(): Nothing { coroutineScope { - launch { hydrator.activate() } - launch { sceneInteractor.currentScene.collect { currentScene -> when (currentScene) { @@ -101,8 +68,6 @@ constructor( ) } } - - launch { quickSettingsContainerViewModel.activate() } } awaitCancellation() 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/domain/interactor/ShadeInteractorSceneContainerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt index 661f2ae5132c..246177e0c46d 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt @@ -35,6 +35,7 @@ import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -45,6 +46,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn /** ShadeInteractor implementation for Scene Container. */ +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class ShadeInteractorSceneContainerImpl @Inject @@ -137,20 +139,18 @@ constructor( override fun expandNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) { if (shadeModeInteractor.isDualShade) { - if (Overlays.QuickSettingsShade in sceneInteractor.currentOverlays.value) { - sceneInteractor.replaceOverlay( - from = Overlays.QuickSettingsShade, - to = Overlays.NotificationsShade, - loggingReason = loggingReason, - transitionKey = transitionKey, - ) - } else { - sceneInteractor.showOverlay( - overlay = Overlays.NotificationsShade, - loggingReason = loggingReason, - transitionKey = transitionKey, - ) - } + // Collapse the quick settings shade if it's expanded (no-op if it isn't). + sceneInteractor.hideOverlay( + overlay = Overlays.QuickSettingsShade, + loggingReason = loggingReason, + transitionKey = transitionKey, + ) + // Expand the notifications shade. + sceneInteractor.showOverlay( + overlay = Overlays.NotificationsShade, + loggingReason = loggingReason, + transitionKey = transitionKey, + ) } else { sceneInteractor.changeScene( toScene = Scenes.Shade, @@ -163,20 +163,18 @@ constructor( override fun expandQuickSettingsShade(loggingReason: String, transitionKey: TransitionKey?) { if (shadeModeInteractor.isDualShade) { - if (Overlays.NotificationsShade in sceneInteractor.currentOverlays.value) { - sceneInteractor.replaceOverlay( - from = Overlays.NotificationsShade, - to = Overlays.QuickSettingsShade, - loggingReason = loggingReason, - transitionKey = transitionKey, - ) - } else { - sceneInteractor.showOverlay( - overlay = Overlays.QuickSettingsShade, - loggingReason = loggingReason, - transitionKey = transitionKey, - ) - } + // Collapse the notifications shade if it's expanded (no-op if it isn't). + sceneInteractor.hideOverlay( + overlay = Overlays.NotificationsShade, + loggingReason = loggingReason, + transitionKey = transitionKey, + ) + // Expand the quick settings shade. + sceneInteractor.showOverlay( + overlay = Overlays.QuickSettingsShade, + loggingReason = loggingReason, + transitionKey = transitionKey, + ) } else { val isSplitShade = shadeModeInteractor.isSplitShade sceneInteractor.changeScene( @@ -199,12 +197,12 @@ constructor( // TODO(b/356596436): Define instant transition instead of snapToScene(). sceneInteractor.snapToScene( toScene = SceneFamilies.Home, - loggingReason = loggingReason + " (collapseNotificationsShade)", + loggingReason = "$loggingReason (collapseNotificationsShade)", ) } else { sceneInteractor.changeScene( toScene = SceneFamilies.Home, - loggingReason = loggingReason + " (collapseNotificationsShade)", + loggingReason = "$loggingReason (collapseNotificationsShade)", transitionKey = transitionKey ?: ToSplitShade.takeIf { shadeModeInteractor.isSplitShade }, ) @@ -233,12 +231,12 @@ constructor( // TODO(b/356596436): Define instant transition instead of snapToScene(). sceneInteractor.snapToScene( toScene = targetScene, - loggingReason = loggingReason + " (collapseQuickSettingsShade)", + loggingReason = "$loggingReason (collapseQuickSettingsShade)", ) } else { sceneInteractor.changeScene( toScene = targetScene, - loggingReason = loggingReason + " (collapseQuickSettingsShade)", + loggingReason = "$loggingReason (collapseQuickSettingsShade)", transitionKey = transitionKey ?: ToSplitShade.takeIf { isSplitShade }, ) } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt index 96128df1b723..51fcf7da3c13 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt @@ -23,8 +23,10 @@ import android.icu.text.DateFormat import android.icu.text.DisplayContext import android.os.UserHandle import android.provider.Settings +import android.view.ViewGroup import androidx.compose.runtime.getValue import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator @@ -40,6 +42,10 @@ import com.android.systemui.shade.domain.interactor.PrivacyChipInteractor import com.android.systemui.shade.domain.interactor.ShadeHeaderClockInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.shared.model.ShadeMode +import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder +import com.android.systemui.statusbar.phone.StatusBarLocation +import com.android.systemui.statusbar.phone.ui.StatusBarIconController +import com.android.systemui.statusbar.phone.ui.TintedIconManager import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel import dagger.assisted.AssistedFactory @@ -67,27 +73,46 @@ constructor( val mobileIconsViewModel: MobileIconsViewModel, private val privacyChipInteractor: PrivacyChipInteractor, private val clockInteractor: ShadeHeaderClockInteractor, + private val tintedIconManagerFactory: TintedIconManager.Factory, + private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory, + val statusBarIconController: StatusBarIconController, + val notificationIconContainerStatusBarViewBinder: NotificationIconContainerStatusBarViewBinder, private val broadcastDispatcher: BroadcastDispatcher, ) : ExclusiveActivatable() { private val hydrator = Hydrator("ShadeHeaderViewModel.hydrator") - val highlightNotificationIcons: Boolean by + val createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager = + tintedIconManagerFactory::create + + val createBatteryMeterViewController: + (ViewGroup, StatusBarLocation) -> BatteryMeterViewController = + batteryMeterViewControllerFactory::create + + val notificationsChipHighlight: HeaderChipHighlight by hydrator.hydratedStateOf( - traceName = "highlightNotificationIcons", - initialValue = false, + traceName = "notificationsChipHighlight", + initialValue = HeaderChipHighlight.None, source = sceneInteractor.currentOverlays.map { overlays -> - Overlays.NotificationsShade in overlays + when { + Overlays.NotificationsShade in overlays -> HeaderChipHighlight.Strong + Overlays.QuickSettingsShade in overlays -> HeaderChipHighlight.Weak + else -> HeaderChipHighlight.None + } }, ) - val highlightQuickSettingsIcons: Boolean by + val quickSettingsChipHighlight: HeaderChipHighlight by hydrator.hydratedStateOf( - traceName = "highlightQuickSettingsIcons", - initialValue = false, + traceName = "quickSettingsChipHighlight", + initialValue = HeaderChipHighlight.None, source = sceneInteractor.currentOverlays.map { overlays -> - Overlays.QuickSettingsShade in overlays + when { + Overlays.QuickSettingsShade in overlays -> HeaderChipHighlight.Strong + Overlays.NotificationsShade in overlays -> HeaderChipHighlight.Weak + else -> HeaderChipHighlight.None + } }, ) @@ -225,6 +250,15 @@ constructor( ) } + /** Represents the background highlight of a header icons chip. */ + sealed interface HeaderChipHighlight { + data object None : HeaderChipHighlight + + data object Weak : HeaderChipHighlight + + data object Strong : HeaderChipHighlight + } + private fun updateDateTexts(invalidateFormats: Boolean) { if (invalidateFormats) { longerDateFormat.value = getFormatFromPattern(longerPattern) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java index ead8f6a1123e..0dfc63ea8619 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java @@ -16,13 +16,9 @@ package com.android.systemui.statusbar; -import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_TRANSITION_FROM_AOD; -import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_TRANSITION_TO_AOD; import static com.android.systemui.keyguard.shared.model.KeyguardState.GONE; import static com.android.systemui.util.kotlin.JavaAdapterKt.combineFlows; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.os.SystemProperties; @@ -30,7 +26,6 @@ import android.os.Trace; import android.text.format.DateFormat; import android.util.FloatProperty; import android.util.Log; -import android.view.Choreographer; import android.view.View; import android.view.animation.Interpolator; @@ -42,8 +37,6 @@ import com.android.compose.animation.scene.OverlayKey; import com.android.compose.animation.scene.SceneKey; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.jank.InteractionJankMonitor; -import com.android.internal.jank.InteractionJankMonitor.Configuration; import com.android.internal.logging.UiEventLogger; import com.android.systemui.DejankUtils; import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; @@ -54,7 +47,6 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; -import com.android.systemui.res.R; import com.android.systemui.scene.data.model.SceneStack; import com.android.systemui.scene.data.model.SceneStackKt; import com.android.systemui.scene.domain.interactor.SceneBackInteractor; @@ -113,7 +105,6 @@ public class StatusBarStateControllerImpl implements private final ArrayList<RankedListener> mListeners = new ArrayList<>(); private final UiEventLogger mUiEventLogger; - private final Lazy<InteractionJankMonitor> mInteractionJankMonitorLazy; private final JavaAdapter mJavaAdapter; private final Lazy<KeyguardInteractor> mKeyguardInteractorLazy; private final Lazy<KeyguardTransitionInteractor> mKeyguardTransitionInteractorLazy; @@ -184,7 +175,6 @@ public class StatusBarStateControllerImpl implements @Inject public StatusBarStateControllerImpl( UiEventLogger uiEventLogger, - Lazy<InteractionJankMonitor> interactionJankMonitorLazy, JavaAdapter javaAdapter, Lazy<KeyguardInteractor> keyguardInteractor, Lazy<KeyguardTransitionInteractor> keyguardTransitionInteractor, @@ -196,7 +186,6 @@ public class StatusBarStateControllerImpl implements Lazy<SceneBackInteractor> sceneBackInteractorLazy, Lazy<AlternateBouncerInteractor> alternateBouncerInteractorLazy) { mUiEventLogger = uiEventLogger; - mInteractionJankMonitorLazy = interactionJankMonitorLazy; mJavaAdapter = javaAdapter; mKeyguardInteractorLazy = keyguardInteractor; mKeyguardTransitionInteractorLazy = keyguardTransitionInteractor; @@ -470,22 +459,6 @@ public class StatusBarStateControllerImpl implements this, SET_DARK_AMOUNT_PROPERTY, mDozeAmountTarget); darkAnimator.setInterpolator(Interpolators.LINEAR); darkAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_WAKEUP); - darkAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationCancel(Animator animation) { - cancelInteractionJankMonitor(); - } - - @Override - public void onAnimationEnd(Animator animation) { - endInteractionJankMonitor(); - } - - @Override - public void onAnimationStart(Animator animation) { - beginInteractionJankMonitor(); - } - }); darkAnimator.start(); return darkAnimator; } @@ -511,42 +484,6 @@ public class StatusBarStateControllerImpl implements return mKeyguardClockInteractorLazy.get().getRenderedClockId(); } - private void beginInteractionJankMonitor() { - final boolean shouldPost = - (mIsDozing && mDozeAmount == 0) || (!mIsDozing && mDozeAmount == 1); - InteractionJankMonitor monitor = mInteractionJankMonitorLazy.get(); - if (monitor != null && mView != null && mView.isAttachedToWindow()) { - if (shouldPost) { - Choreographer.getInstance().postCallback( - Choreographer.CALLBACK_ANIMATION, this::beginInteractionJankMonitor, null); - } else { - Configuration.Builder builder = Configuration.Builder.withView(getCujType(), mView) - .setTag(getClockId()) - .setDeferMonitorForAnimationStart(false); - monitor.begin(builder); - } - } - } - - private void endInteractionJankMonitor() { - InteractionJankMonitor monitor = mInteractionJankMonitorLazy.get(); - if (monitor == null) { - return; - } - monitor.end(getCujType()); - } - - private void cancelInteractionJankMonitor() { - InteractionJankMonitor monitor = mInteractionJankMonitorLazy.get(); - if (monitor == null) { - return; - } - monitor.cancel(getCujType()); - } - - private int getCujType() { - return mIsDozing ? CUJ_LOCKSCREEN_TRANSITION_TO_AOD : CUJ_LOCKSCREEN_TRANSITION_FROM_AOD; - } @Override public boolean goingToFullShade() { 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/promoted/AODPromotedNotification.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt index 33c71d4a9c5a..098d537fc225 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.promoted import android.app.Flags +import android.app.Notification import android.view.LayoutInflater import android.view.View import android.view.View.GONE @@ -49,6 +50,7 @@ import com.android.internal.widget.CachingIconView import com.android.internal.widget.ImageFloatingTextView import com.android.internal.widget.NotificationExpandButton import com.android.internal.widget.NotificationProgressBar +import com.android.internal.widget.NotificationProgressModel import com.android.internal.widget.NotificationRowIconView import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.res.R as systemuiR @@ -176,17 +178,17 @@ private class AODPromotedNotificationViewUpdater(root: View) { private val verificationIcon: ImageView? = root.findViewById(R.id.verification_icon) private val verificationText: TextView? = root.findViewById(R.id.verification_text) - private var oldProgressStub = root.findViewById<View>(R.id.progress) as? ViewStub - private var oldProgress: ProgressBar? = null - private val newProgress = root.findViewById<View>(R.id.progress) as? NotificationProgressBar + private var oldProgressBarStub = root.findViewById<View>(R.id.progress) as? ViewStub + private var oldProgressBar: ProgressBar? = null + private val newProgressBar = root.findViewById<View>(R.id.progress) as? NotificationProgressBar fun update(content: PromotedNotificationContentModel) { when (content.style) { Style.Base -> updateBase(content) - Style.BigPicture -> updateBigPicture(content) - Style.BigText -> updateBigText(content) - Style.Call -> updateCall(content) - Style.Progress -> updateProgress(content) + Style.BigPicture -> updateBigPictureStyle(content) + Style.BigText -> updateBigTextStyle(content) + Style.Call -> updateCallStyle(content) + Style.Progress -> updateProgressStyle(content) Style.Ineligible -> {} } } @@ -194,42 +196,72 @@ private class AODPromotedNotificationViewUpdater(root: View) { private fun updateBase( content: PromotedNotificationContentModel, textView: ImageFloatingTextView? = null, + showOldProgress: Boolean = true, ) { updateHeader(content) updateTitle(title, content) updateText(textView ?: text, content) + + if (showOldProgress) { + updateOldProgressBar(content) + } } - private fun updateBigPicture(content: PromotedNotificationContentModel) { + private fun updateBigPictureStyle(content: PromotedNotificationContentModel) { updateBase(content) bigPicture?.visibility = GONE } - private fun updateBigText(content: PromotedNotificationContentModel) { + private fun updateBigTextStyle(content: PromotedNotificationContentModel) { updateBase(content, textView = bigText) } - private fun updateCall(content: PromotedNotificationContentModel) { + private fun updateCallStyle(content: PromotedNotificationContentModel) { updateConversationHeader(content) updateText(text, content) } - private fun updateProgress(content: PromotedNotificationContentModel) { - updateBase(content) + private fun updateProgressStyle(content: PromotedNotificationContentModel) { + updateBase(content, showOldProgress = false) updateNewProgressBar(content) } + private fun updateOldProgressBar(content: PromotedNotificationContentModel) { + if ( + content.oldProgress == null || + content.oldProgress.max == 0 || + content.oldProgress.isIndeterminate + ) { + oldProgressBar?.visibility = GONE + return + } + + inflateOldProgressBar() + + val oldProgressBar = oldProgressBar ?: return + + oldProgressBar.progress = content.oldProgress.progress + oldProgressBar.max = content.oldProgress.max + oldProgressBar.isIndeterminate = content.oldProgress.isIndeterminate + oldProgressBar.visibility = VISIBLE + } + private fun updateNewProgressBar(content: PromotedNotificationContentModel) { notificationProgressStartIcon?.visibility = GONE notificationProgressEndIcon?.visibility = GONE - content.progress?.let { - newProgress?.setProgressModel(it.toBundle()) - newProgress?.visibility = VISIBLE - } ?: run { newProgress?.visibility = GONE } + + val newProgressBar = newProgressBar ?: return + + if (content.newProgress != null && !content.newProgress.isIndeterminate) { + newProgressBar.setProgressModel(content.newProgress.toSkeleton().toBundle()) + newProgressBar.visibility = VISIBLE + } else { + newProgressBar.visibility = GONE + } } private fun updateHeader(content: PromotedNotificationContentModel) { @@ -335,6 +367,15 @@ private class AODPromotedNotificationViewUpdater(root: View) { chronometerStub = null } + private fun inflateOldProgressBar() { + if (oldProgressBar != null) { + return + } + + oldProgressBar = oldProgressBarStub?.inflate() as ProgressBar + oldProgressBarStub = null + } + private fun updateText( view: ImageFloatingTextView?, content: PromotedNotificationContentModel, @@ -351,7 +392,7 @@ private class AODPromotedNotificationViewUpdater(root: View) { setTextViewColor(view, color) if (text != null && text.isNotEmpty()) { - view?.text = text + view?.text = text.toSkeleton() view?.visibility = VISIBLE } else { view?.text = "" @@ -364,6 +405,38 @@ private class AODPromotedNotificationViewUpdater(root: View) { } } +private fun CharSequence.toSkeleton(): CharSequence { + return this.toString() +} + +private fun NotificationProgressModel.toSkeleton(): NotificationProgressModel { + if (isIndeterminate) { + return NotificationProgressModel(/* indeterminateColor= */ SecondaryText.colorInt) + } + + return NotificationProgressModel( + listOf(Notification.ProgressStyle.Segment(progressMax).toSkeleton()), + points.map { it.toSkeleton() }.toList(), + progress, + /* isStyledByProgress = */ true, + /* segmentsFallbackColor = */ SecondaryText.colorInt, + ) +} + +private fun Notification.ProgressStyle.Segment.toSkeleton(): Notification.ProgressStyle.Segment { + return Notification.ProgressStyle.Segment(length).also { + it.id = id + it.color = SecondaryText.colorInt + } +} + +private fun Notification.ProgressStyle.Point.toSkeleton(): Notification.ProgressStyle.Point { + return Notification.ProgressStyle.Point(position).also { + it.id = id + it.color = SecondaryText.colorInt + } +} + private enum class AodPromotedNotificationColor(colorUInt: UInt) { Background(0x00000000u), PrimaryText(0xFFFFFFFFu), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt index 24d071c83a5e..035edd9711bd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt @@ -21,6 +21,9 @@ import android.app.Notification.BigPictureStyle import android.app.Notification.BigTextStyle import android.app.Notification.CallStyle import android.app.Notification.EXTRA_CHRONOMETER_COUNT_DOWN +import android.app.Notification.EXTRA_PROGRESS +import android.app.Notification.EXTRA_PROGRESS_INDETERMINATE +import android.app.Notification.EXTRA_PROGRESS_MAX import android.app.Notification.EXTRA_SUB_TEXT import android.app.Notification.EXTRA_TEXT import android.app.Notification.EXTRA_TITLE @@ -34,6 +37,7 @@ import com.android.systemui.statusbar.notification.promoted.AutomaticPromotionCo import com.android.systemui.statusbar.notification.promoted.AutomaticPromotionCoordinator.Companion.EXTRA_WAS_AUTOMATICALLY_PROMOTED import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Companion.isPromotedForStatusBarChip +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.OldProgress import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.When import javax.inject.Inject @@ -90,6 +94,7 @@ constructor( contentBuilder.title = notification.title() contentBuilder.text = notification.text() contentBuilder.skeletonLargeIcon = null // TODO + contentBuilder.oldProgress = notification.oldProgress() val colorsFromNotif = recoveredBuilder.getColors(/* header= */ false) contentBuilder.colors = @@ -126,6 +131,21 @@ private fun Notification.shortCriticalText(): String? { private fun Notification.chronometerCountDown(): Boolean = extras?.getBoolean(EXTRA_CHRONOMETER_COUNT_DOWN, /* defaultValue= */ false) ?: false +private fun Notification.oldProgress(): OldProgress? { + val progress = progress() ?: return null + val max = progressMax() ?: return null + val isIndeterminate = progressIndeterminate() ?: return null + + return OldProgress(progress = progress, max = max, isIndeterminate = isIndeterminate) +} + +private fun Notification.progress(): Int? = extras?.getInt(EXTRA_PROGRESS) + +private fun Notification.progressMax(): Int? = extras?.getInt(EXTRA_PROGRESS_MAX) + +private fun Notification.progressIndeterminate(): Boolean? = + extras?.getBoolean(EXTRA_PROGRESS_INDETERMINATE) + private fun Notification.extractWhen(): When? { val time = `when` val showsTime = showsTime() @@ -191,5 +211,5 @@ private fun CallStyle.extractContent(contentBuilder: PromotedNotificationContent private fun ProgressStyle.extractContent(contentBuilder: PromotedNotificationContentModel.Builder) { // TODO: Create NotificationProgressModel.toSkeleton, or something similar. - contentBuilder.progress = createProgressModel(0xffffffff.toInt(), 0xff000000.toInt()) + contentBuilder.newProgress = createProgressModel(0xffffffff.toInt(), 0xff000000.toInt()) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt index 4ccdc65ba91a..468934791525 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.promoted import androidx.constraintlayout.widget.ConstraintSet +import com.android.systemui.keyguard.ui.view.layout.sections.AodPromotedNotificationSection import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel.ERROR import com.android.systemui.log.core.LogLevel.INFO @@ -25,6 +26,7 @@ import com.android.systemui.statusbar.notification.logKey import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import javax.inject.Inject +@OptIn(ExperimentalStdlibApi::class) class PromotedNotificationLogger @Inject constructor(@PromotedNotificationLog private val buffer: LogBuffer) { @@ -92,20 +94,44 @@ constructor(@PromotedNotificationLog private val buffer: LogBuffer) { buffer.log(AOD_VIEW_BINDER_TAG, INFO, "binder unbound notification") } - fun logSectionAddedViews() { - buffer.log(AOD_SECTION_TAG, INFO, "section added views") + fun logSectionCreated(section: AodPromotedNotificationSection) { + buffer.log( + AOD_SECTION_TAG, + INFO, + "section ${System.identityHashCode(section).toHexString()} created", + ) } - fun logSectionBoundData() { - buffer.log(AOD_SECTION_TAG, INFO, "section bound data") + fun logSectionAddedViews(section: AodPromotedNotificationSection) { + buffer.log( + AOD_SECTION_TAG, + INFO, + "section ${System.identityHashCode(section).toHexString()} added views", + ) } - fun logSectionAppliedConstraints() { - buffer.log(AOD_SECTION_TAG, INFO, "section applied constraints") + fun logSectionBoundData(section: AodPromotedNotificationSection) { + buffer.log( + AOD_SECTION_TAG, + INFO, + "section ${System.identityHashCode(section).toHexString()} bound data", + ) } - fun logSectionRemovedViews() { - buffer.log(AOD_SECTION_TAG, INFO, "section removed views") + fun logSectionAppliedConstraints(section: AodPromotedNotificationSection) { + buffer.log( + AOD_SECTION_TAG, + INFO, + "section ${System.identityHashCode(section).toHexString()} applied constraints", + ) + } + + fun logSectionRemovedViews(section: AodPromotedNotificationSection) { + buffer.log( + AOD_SECTION_TAG, + INFO, + "section ${System.identityHashCode(section).toHexString()} removed views", + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt index 3dacae2114b0..58da5286dd71 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt @@ -51,6 +51,7 @@ data class PromotedNotificationContentModel( val title: CharSequence?, val text: CharSequence?, val skeletonLargeIcon: Icon?, // TODO(b/377568176): Make into an IconModel. + val oldProgress: OldProgress?, val colors: Colors, val style: Style, @@ -61,7 +62,7 @@ data class PromotedNotificationContentModel( val verificationText: CharSequence?, // for ProgressStyle: - val progress: NotificationProgressModel?, + val newProgress: NotificationProgressModel?, ) { class Builder(val key: String) { var wasPromotedAutomatically: Boolean = false @@ -75,6 +76,7 @@ data class PromotedNotificationContentModel( var title: CharSequence? = null var text: CharSequence? = null var skeletonLargeIcon: Icon? = null + var oldProgress: OldProgress? = null var style: Style = Style.Ineligible var colors: Colors = Colors(backgroundColor = 0, primaryTextColor = 0) @@ -85,7 +87,7 @@ data class PromotedNotificationContentModel( var verificationText: CharSequence? = null // for ProgressStyle: - var progress: NotificationProgressModel? = null + var newProgress: NotificationProgressModel? = null fun build() = PromotedNotificationContentModel( @@ -101,13 +103,14 @@ data class PromotedNotificationContentModel( title = title, text = text, skeletonLargeIcon = skeletonLargeIcon, + oldProgress = oldProgress, colors = colors, style = style, personIcon = personIcon, personName = personName, verificationIcon = verificationIcon, verificationText = verificationText, - progress = progress, + newProgress = newProgress, ) } @@ -129,6 +132,9 @@ data class PromotedNotificationContentModel( /** The colors used to display the notification. */ data class Colors(@ColorInt val backgroundColor: Int, @ColorInt val primaryTextColor: Int) + /** The fields needed to render the old-style progress bar. */ + data class OldProgress(val progress: Int, val max: Int, val isIndeterminate: Boolean) + /** The promotion-eligible style of a notification, or [Style.Ineligible] if not. */ enum class Style { Base, // style == null diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/PromotedNotificationViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/PromotedNotificationViewModel.kt index f265e0ff33f8..56057fb00e45 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/PromotedNotificationViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/PromotedNotificationViewModel.kt @@ -54,5 +54,5 @@ class PromotedNotificationViewModel( val verificationText: Flow<CharSequence?> = content.map { it.verificationText } // for ProgressStyle: - val progress: Flow<NotificationProgressModel?> = content.map { it.progress } + val progress: Flow<NotificationProgressModel?> = content.map { it.newProgress } } 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/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/bluetooth/qsdialog/BluetoothDetailsContentManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentManagerTest.kt new file mode 100644 index 000000000000..6ed990d513cb --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentManagerTest.kt @@ -0,0 +1,461 @@ +/* + * 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.bluetooth.qsdialog + +import android.graphics.drawable.Drawable +import android.testing.TestableLooper +import android.view.LayoutInflater +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.logging.UiEventLogger +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.DialogTransitionAnimator +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.model.SysUiState +import com.android.systemui.res.R +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.statusbar.phone.SystemUIDialogManager +import com.android.systemui.testKosmos +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runCurrent +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.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyLong +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +class BluetoothDetailsContentManagerTest : SysuiTestCase() { + companion object { + const val DEVICE_NAME = "device" + const val DEVICE_CONNECTION_SUMMARY = "active" + const val ENABLED = true + const val CONTENT_HEIGHT = WRAP_CONTENT + } + + @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() + + private val cachedBluetoothDevice = mock<CachedBluetoothDevice>() + + private val bluetoothTileDialogCallback = mock<BluetoothTileDialogCallback>() + + private val drawable = mock<Drawable>() + + private val uiEventLogger = mock<UiEventLogger>() + + private val logger = mock<BluetoothTileDialogLogger>() + + private val sysuiDialogFactory = mock<SystemUIDialog.Factory>() + private val dialogManager = mock<SystemUIDialogManager>() + private val sysuiState = mock<SysUiState>() + private val dialogTransitionAnimator = mock<DialogTransitionAnimator>() + + private val fakeSystemClock = FakeSystemClock() + + private val uiProperties = + BluetoothTileDialogViewModel.UiProperties.build( + isBluetoothEnabled = ENABLED, + isAutoOnToggleFeatureAvailable = ENABLED, + ) + + private lateinit var icon: Pair<Drawable, String> + private lateinit var mBluetoothDetailsContentManager: BluetoothDetailsContentManager + private lateinit var deviceItem: DeviceItem + private lateinit var contentView: View + + private val kosmos = testKosmos() + + @Before + fun setUp() { + with(kosmos) { + contentView = + LayoutInflater.from(mContext).inflate(R.layout.bluetooth_tile_dialog, null) + + whenever(sysuiState.setFlag(anyLong(), anyBoolean())).thenReturn(sysuiState) + + mBluetoothDetailsContentManager = + BluetoothDetailsContentManager( + uiProperties, + CONTENT_HEIGHT, + bluetoothTileDialogCallback, + /* isInDialog= */ true, + {}, + testDispatcher, + fakeSystemClock, + uiEventLogger, + logger, + ) + + whenever(sysuiDialogFactory.create(any<SystemUIDialog.Delegate>(), any())).thenAnswer { + SystemUIDialog( + mContext, + 0, + SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK, + dialogManager, + sysuiState, + fakeBroadcastDispatcher, + dialogTransitionAnimator, + it.getArgument(0), + ) + } + + icon = Pair(drawable, DEVICE_NAME) + deviceItem = + DeviceItem( + type = DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE, + cachedBluetoothDevice = cachedBluetoothDevice, + deviceName = DEVICE_NAME, + connectionSummary = DEVICE_CONNECTION_SUMMARY, + iconWithDescription = icon, + background = null, + ) + whenever(cachedBluetoothDevice.isBusy).thenReturn(false) + } + } + + @Test + fun testShowDialog_createRecyclerViewWithAdapter() { + mBluetoothDetailsContentManager.bind(contentView) + mBluetoothDetailsContentManager.start() + + val recyclerView = contentView.requireViewById<RecyclerView>(R.id.device_list) + + assertThat(recyclerView).isNotNull() + assertThat(recyclerView.visibility).isEqualTo(VISIBLE) + assertThat(recyclerView.adapter).isNotNull() + assertThat(recyclerView.layoutManager is LinearLayoutManager).isTrue() + mBluetoothDetailsContentManager.releaseView() + } + + @Test + fun testShowDialog_displayBluetoothDevice() { + with(kosmos) { + testScope.runTest { + mBluetoothDetailsContentManager.bind(contentView) + mBluetoothDetailsContentManager.start() + fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE) + mBluetoothDetailsContentManager.onDeviceItemUpdated( + listOf(deviceItem), + showSeeAll = false, + showPairNewDevice = false, + ) + + val recyclerView = contentView.requireViewById<RecyclerView>(R.id.device_list) + val adapter = recyclerView?.adapter as BluetoothDetailsContentManager.Adapter + assertThat(adapter.itemCount).isEqualTo(1) + assertThat(adapter.getItem(0).deviceName).isEqualTo(DEVICE_NAME) + assertThat(adapter.getItem(0).connectionSummary) + .isEqualTo(DEVICE_CONNECTION_SUMMARY) + assertThat(adapter.getItem(0).iconWithDescription).isEqualTo(icon) + mBluetoothDetailsContentManager.releaseView() + } + } + } + + @Test + fun testDeviceItemViewHolder_cachedDeviceNotBusy() { + with(kosmos) { + testScope.runTest { + deviceItem.isEnabled = true + + val view = + LayoutInflater.from(mContext) + .inflate(R.layout.bluetooth_device_item, null, false) + val viewHolder = + mBluetoothDetailsContentManager.Adapter().DeviceItemViewHolder(view) + viewHolder.bind(deviceItem) + val container = view.requireViewById<View>(R.id.bluetooth_device_row) + + assertThat(container).isNotNull() + assertThat(container.isEnabled).isTrue() + assertThat(container.hasOnClickListeners()).isTrue() + val value by collectLastValue(mBluetoothDetailsContentManager.deviceItemClick) + runCurrent() + container.performClick() + runCurrent() + assertThat(value).isNotNull() + value?.let { + assertThat(it.target).isEqualTo(DeviceItemClick.Target.ENTIRE_ROW) + assertThat(it.clickedView).isEqualTo(container) + assertThat(it.deviceItem).isEqualTo(deviceItem) + } + } + } + } + + @Test + fun testDeviceItemViewHolder_cachedDeviceBusy() { + with(kosmos) { + deviceItem.isEnabled = false + + val view = + LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false) + val viewHolder = + BluetoothDetailsContentManager( + uiProperties, + CONTENT_HEIGHT, + bluetoothTileDialogCallback, + /* isInDialog= */ true, + {}, + testDispatcher, + fakeSystemClock, + uiEventLogger, + logger, + ) + .Adapter() + .DeviceItemViewHolder(view) + viewHolder.bind(deviceItem) + val container = view.requireViewById<View>(R.id.bluetooth_device_row) + + assertThat(container).isNotNull() + assertThat(container.isEnabled).isFalse() + assertThat(container.hasOnClickListeners()).isTrue() + } + } + + @Test + fun testDeviceItemViewHolder_clickActionIcon() { + with(kosmos) { + testScope.runTest { + deviceItem.isEnabled = true + + val view = + LayoutInflater.from(mContext) + .inflate(R.layout.bluetooth_device_item, null, false) + val viewHolder = + mBluetoothDetailsContentManager.Adapter().DeviceItemViewHolder(view) + viewHolder.bind(deviceItem) + val actionIconView = view.requireViewById<View>(R.id.gear_icon) + + assertThat(actionIconView).isNotNull() + assertThat(actionIconView.hasOnClickListeners()).isTrue() + val value by collectLastValue(mBluetoothDetailsContentManager.deviceItemClick) + runCurrent() + actionIconView.performClick() + runCurrent() + assertThat(value).isNotNull() + value?.let { + assertThat(it.target).isEqualTo(DeviceItemClick.Target.ACTION_ICON) + assertThat(it.clickedView).isEqualTo(actionIconView) + assertThat(it.deviceItem).isEqualTo(deviceItem) + } + } + } + } + + @Test + fun testOnDeviceUpdated_hideSeeAll_showPairNew() { + with(kosmos) { + testScope.runTest { + mBluetoothDetailsContentManager.bind(contentView) + mBluetoothDetailsContentManager.start() + fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE) + mBluetoothDetailsContentManager.onDeviceItemUpdated( + listOf(deviceItem), + showSeeAll = false, + showPairNewDevice = true, + ) + + val seeAllButton = contentView.requireViewById<View>(R.id.see_all_button) + val pairNewButton = contentView.requireViewById<View>(R.id.pair_new_device_button) + val recyclerView = contentView.requireViewById<RecyclerView>(R.id.device_list) + val adapter = recyclerView?.adapter as BluetoothDetailsContentManager.Adapter + val scrollViewContent = contentView.requireViewById<View>(R.id.scroll_view) + + assertThat(seeAllButton).isNotNull() + assertThat(seeAllButton.visibility).isEqualTo(GONE) + assertThat(pairNewButton).isNotNull() + assertThat(pairNewButton.visibility).isEqualTo(VISIBLE) + assertThat(adapter.itemCount).isEqualTo(1) + assertThat(scrollViewContent.layoutParams.height).isEqualTo(WRAP_CONTENT) + mBluetoothDetailsContentManager.releaseView() + } + } + } + + @Test + fun testShowDialog_cachedHeightLargerThanMinHeight_displayFromCachedHeight() { + with(kosmos) { + testScope.runTest { + val cachedHeight = Int.MAX_VALUE + val contentManager = + BluetoothDetailsContentManager( + BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED), + cachedHeight, + bluetoothTileDialogCallback, + /* isInDialog= */ true, + {}, + testDispatcher, + fakeSystemClock, + uiEventLogger, + logger, + ) + contentManager.bind(contentView) + contentManager.start() + assertThat(contentView.requireViewById<View>(R.id.scroll_view).layoutParams.height) + .isEqualTo(cachedHeight) + contentManager.releaseView() + } + } + } + + @Test + fun testShowDialog_cachedHeightLessThanMinHeight_displayFromUiProperties() { + with(kosmos) { + testScope.runTest { + val contentManager = + BluetoothDetailsContentManager( + BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED), + MATCH_PARENT, + bluetoothTileDialogCallback, + /* isInDialog= */ true, + {}, + testDispatcher, + fakeSystemClock, + uiEventLogger, + logger, + ) + contentManager.bind(contentView) + contentManager.start() + assertThat(contentView.requireViewById<View>(R.id.scroll_view).layoutParams.height) + .isGreaterThan(MATCH_PARENT) + contentManager.releaseView() + } + } + } + + @Test + fun testShowDialog_bluetoothEnabled_autoOnToggleGone() { + with(kosmos) { + testScope.runTest { + val contentManager = + BluetoothDetailsContentManager( + BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED), + MATCH_PARENT, + bluetoothTileDialogCallback, + /* isInDialog= */ true, + {}, + testDispatcher, + fakeSystemClock, + uiEventLogger, + logger, + ) + contentManager.bind(contentView) + contentManager.start() + assertThat( + contentView + .requireViewById<View>(R.id.bluetooth_auto_on_toggle_layout) + .visibility + ) + .isEqualTo(GONE) + contentManager.releaseView() + } + } + } + + @Test + fun testOnAudioSharingButtonUpdated_visibleActive_activateButton() { + with(kosmos) { + testScope.runTest { + mBluetoothDetailsContentManager.bind(contentView) + mBluetoothDetailsContentManager.start() + fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE) + mBluetoothDetailsContentManager.onAudioSharingButtonUpdated( + visibility = VISIBLE, + label = null, + isActive = true, + ) + + val audioSharingButton = + contentView.requireViewById<View>(R.id.audio_sharing_button) + + assertThat(audioSharingButton).isNotNull() + assertThat(audioSharingButton.visibility).isEqualTo(VISIBLE) + assertThat(audioSharingButton.isActivated).isTrue() + mBluetoothDetailsContentManager.releaseView() + } + } + } + + @Test + fun testOnAudioSharingButtonUpdated_visibleNotActive_inactivateButton() { + with(kosmos) { + testScope.runTest { + mBluetoothDetailsContentManager.bind(contentView) + mBluetoothDetailsContentManager.start() + fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE) + mBluetoothDetailsContentManager.onAudioSharingButtonUpdated( + visibility = VISIBLE, + label = null, + isActive = false, + ) + + val audioSharingButton = + contentView.requireViewById<View>(R.id.audio_sharing_button) + + assertThat(audioSharingButton).isNotNull() + assertThat(audioSharingButton.visibility).isEqualTo(VISIBLE) + assertThat(audioSharingButton.isActivated).isFalse() + mBluetoothDetailsContentManager.releaseView() + } + } + } + + @Test + fun testOnAudioSharingButtonUpdated_gone_inactivateButton() { + with(kosmos) { + testScope.runTest { + mBluetoothDetailsContentManager.bind(contentView) + mBluetoothDetailsContentManager.start() + fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE) + mBluetoothDetailsContentManager.onAudioSharingButtonUpdated( + visibility = GONE, + label = null, + isActive = false, + ) + + val audioSharingButton = + contentView.requireViewById<View>(R.id.audio_sharing_button) + + assertThat(audioSharingButton).isNotNull() + assertThat(audioSharingButton.visibility).isEqualTo(GONE) + assertThat(audioSharingButton.isActivated).isFalse() + mBluetoothDetailsContentManager.releaseView() + } + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt index 4396b0a42ae6..ffc75188ffa1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt @@ -16,47 +16,34 @@ package com.android.systemui.bluetooth.qsdialog -import android.graphics.drawable.Drawable import android.testing.TestableLooper -import android.view.LayoutInflater -import android.view.View -import android.view.View.GONE -import android.view.View.VISIBLE -import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger -import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogTransitionAnimator -import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.model.SysUiState -import com.android.systemui.res.R import com.android.systemui.shade.data.repository.shadeDialogContextInteractor import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.SystemUIDialogManager import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever -import com.android.systemui.util.time.FakeSystemClock -import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.test.TestCoroutineScheduler import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runCurrent -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.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyLong import org.mockito.Mock -import org.mockito.Mockito.`when` +import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule @@ -73,33 +60,31 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() - @Mock private lateinit var cachedBluetoothDevice: CachedBluetoothDevice + @Mock + private lateinit var bluetoothDetailsContentManagerFactory: + BluetoothDetailsContentManager.Factory - @Mock private lateinit var bluetoothTileDialogCallback: BluetoothTileDialogCallback + @Mock private lateinit var bluetoothDetailsContentManager: BluetoothDetailsContentManager - @Mock private lateinit var drawable: Drawable + @Mock private lateinit var bluetoothTileDialogCallback: BluetoothTileDialogCallback @Mock private lateinit var uiEventLogger: UiEventLogger - @Mock private lateinit var logger: BluetoothTileDialogLogger + @Mock private lateinit var sysuiDialogFactory: SystemUIDialog.Factory + @Mock private lateinit var dialogManager: SystemUIDialogManager + @Mock private lateinit var sysuiState: SysUiState + @Mock private lateinit var dialogTransitionAnimator: DialogTransitionAnimator private val uiProperties = BluetoothTileDialogViewModel.UiProperties.build( isBluetoothEnabled = ENABLED, isAutoOnToggleFeatureAvailable = ENABLED, ) - @Mock private lateinit var sysuiDialogFactory: SystemUIDialog.Factory - @Mock private lateinit var dialogManager: SystemUIDialogManager - @Mock private lateinit var sysuiState: SysUiState - @Mock private lateinit var dialogTransitionAnimator: DialogTransitionAnimator - - private val fakeSystemClock = FakeSystemClock() + private lateinit var scheduler: TestCoroutineScheduler private lateinit var dispatcher: CoroutineDispatcher private lateinit var testScope: TestScope - private lateinit var icon: Pair<Drawable, String> private lateinit var mBluetoothTileDialogDelegate: BluetoothTileDialogDelegate - private lateinit var deviceItem: DeviceItem private val kosmos = testKosmos() @@ -116,12 +101,10 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { CONTENT_HEIGHT, bluetoothTileDialogCallback, {}, - dispatcher, - fakeSystemClock, uiEventLogger, - logger, sysuiDialogFactory, kosmos.shadeDialogContextInteractor, + bluetoothDetailsContentManagerFactory, ) whenever(sysuiDialogFactory.create(any(SystemUIDialog.Delegate::class.java), any())) @@ -138,17 +121,16 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { ) } - icon = Pair(drawable, DEVICE_NAME) - deviceItem = - DeviceItem( - type = DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE, - cachedBluetoothDevice = cachedBluetoothDevice, - deviceName = DEVICE_NAME, - connectionSummary = DEVICE_CONNECTION_SUMMARY, - iconWithDescription = icon, - background = null, + whenever( + bluetoothDetailsContentManagerFactory.create( + any(), + anyInt(), + any(), + anyBoolean(), + any(), + ) ) - `when`(cachedBluetoothDevice.isBusy).thenReturn(false) + .thenReturn(bluetoothDetailsContentManager) } @Test @@ -156,287 +138,9 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { val dialog = mBluetoothTileDialogDelegate.createDialog() dialog.show() - val recyclerView = dialog.requireViewById<RecyclerView>(R.id.device_list) - - assertThat(recyclerView).isNotNull() - assertThat(recyclerView.visibility).isEqualTo(VISIBLE) - assertThat(recyclerView.adapter).isNotNull() - assertThat(recyclerView.layoutManager is LinearLayoutManager).isTrue() + verify(bluetoothDetailsContentManager).bind(any()) + verify(bluetoothDetailsContentManager).start() dialog.dismiss() - } - - @Test - fun testShowDialog_displayBluetoothDevice() { - testScope.runTest { - val dialog = mBluetoothTileDialogDelegate.createDialog() - dialog.show() - fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE) - mBluetoothTileDialogDelegate.onDeviceItemUpdated( - dialog, - listOf(deviceItem), - showSeeAll = false, - showPairNewDevice = false, - ) - - val recyclerView = dialog.requireViewById<RecyclerView>(R.id.device_list) - val adapter = recyclerView?.adapter as BluetoothTileDialogDelegate.Adapter - assertThat(adapter.itemCount).isEqualTo(1) - assertThat(adapter.getItem(0).deviceName).isEqualTo(DEVICE_NAME) - assertThat(adapter.getItem(0).connectionSummary).isEqualTo(DEVICE_CONNECTION_SUMMARY) - assertThat(adapter.getItem(0).iconWithDescription).isEqualTo(icon) - dialog.dismiss() - } - } - - @Test - fun testDeviceItemViewHolder_cachedDeviceNotBusy() { - testScope.runTest { - deviceItem.isEnabled = true - - val view = - LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false) - val viewHolder = mBluetoothTileDialogDelegate.Adapter().DeviceItemViewHolder(view) - viewHolder.bind(deviceItem) - val container = view.requireViewById<View>(R.id.bluetooth_device_row) - - assertThat(container).isNotNull() - assertThat(container.isEnabled).isTrue() - assertThat(container.hasOnClickListeners()).isTrue() - val value by collectLastValue(mBluetoothTileDialogDelegate.deviceItemClick) - runCurrent() - container.performClick() - runCurrent() - assertThat(value).isNotNull() - value?.let { - assertThat(it.target).isEqualTo(DeviceItemClick.Target.ENTIRE_ROW) - assertThat(it.clickedView).isEqualTo(container) - assertThat(it.deviceItem).isEqualTo(deviceItem) - } - } - } - - @Test - fun testDeviceItemViewHolder_cachedDeviceBusy() { - deviceItem.isEnabled = false - - val view = - LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false) - val viewHolder = - BluetoothTileDialogDelegate( - uiProperties, - CONTENT_HEIGHT, - bluetoothTileDialogCallback, - {}, - dispatcher, - fakeSystemClock, - uiEventLogger, - logger, - sysuiDialogFactory, - kosmos.shadeDialogContextInteractor, - ) - .Adapter() - .DeviceItemViewHolder(view) - viewHolder.bind(deviceItem) - val container = view.requireViewById<View>(R.id.bluetooth_device_row) - - assertThat(container).isNotNull() - assertThat(container.isEnabled).isFalse() - assertThat(container.hasOnClickListeners()).isTrue() - } - - @Test - fun testDeviceItemViewHolder_clickActionIcon() { - testScope.runTest { - deviceItem.isEnabled = true - - val view = - LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false) - val viewHolder = mBluetoothTileDialogDelegate.Adapter().DeviceItemViewHolder(view) - viewHolder.bind(deviceItem) - val actionIconView = view.requireViewById<View>(R.id.gear_icon) - - assertThat(actionIconView).isNotNull() - assertThat(actionIconView.hasOnClickListeners()).isTrue() - val value by collectLastValue(mBluetoothTileDialogDelegate.deviceItemClick) - runCurrent() - actionIconView.performClick() - runCurrent() - assertThat(value).isNotNull() - value?.let { - assertThat(it.target).isEqualTo(DeviceItemClick.Target.ACTION_ICON) - assertThat(it.clickedView).isEqualTo(actionIconView) - assertThat(it.deviceItem).isEqualTo(deviceItem) - } - } - } - - @Test - fun testOnDeviceUpdated_hideSeeAll_showPairNew() { - testScope.runTest { - val dialog = mBluetoothTileDialogDelegate.createDialog() - dialog.show() - fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE) - mBluetoothTileDialogDelegate.onDeviceItemUpdated( - dialog, - listOf(deviceItem), - showSeeAll = false, - showPairNewDevice = true, - ) - - val seeAllButton = dialog.requireViewById<View>(R.id.see_all_button) - val pairNewButton = dialog.requireViewById<View>(R.id.pair_new_device_button) - val recyclerView = dialog.requireViewById<RecyclerView>(R.id.device_list) - val adapter = recyclerView?.adapter as BluetoothTileDialogDelegate.Adapter - val scrollViewContent = dialog.requireViewById<View>(R.id.scroll_view) - - assertThat(seeAllButton).isNotNull() - assertThat(seeAllButton.visibility).isEqualTo(GONE) - assertThat(pairNewButton).isNotNull() - assertThat(pairNewButton.visibility).isEqualTo(VISIBLE) - assertThat(adapter.itemCount).isEqualTo(1) - assertThat(scrollViewContent.layoutParams.height).isEqualTo(WRAP_CONTENT) - dialog.dismiss() - } - } - - @Test - fun testShowDialog_cachedHeightLargerThanMinHeight_displayFromCachedHeight() { - testScope.runTest { - val cachedHeight = Int.MAX_VALUE - val dialog = - BluetoothTileDialogDelegate( - BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED), - cachedHeight, - bluetoothTileDialogCallback, - {}, - dispatcher, - fakeSystemClock, - uiEventLogger, - logger, - sysuiDialogFactory, - kosmos.shadeDialogContextInteractor, - ) - .createDialog() - dialog.show() - assertThat(dialog.requireViewById<View>(R.id.scroll_view).layoutParams.height) - .isEqualTo(cachedHeight) - dialog.dismiss() - } - } - - @Test - fun testShowDialog_cachedHeightLessThanMinHeight_displayFromUiProperties() { - testScope.runTest { - val dialog = - BluetoothTileDialogDelegate( - BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED), - MATCH_PARENT, - bluetoothTileDialogCallback, - {}, - dispatcher, - fakeSystemClock, - uiEventLogger, - logger, - sysuiDialogFactory, - kosmos.shadeDialogContextInteractor, - ) - .createDialog() - dialog.show() - assertThat(dialog.requireViewById<View>(R.id.scroll_view).layoutParams.height) - .isGreaterThan(MATCH_PARENT) - dialog.dismiss() - } - } - - @Test - fun testShowDialog_bluetoothEnabled_autoOnToggleGone() { - testScope.runTest { - val dialog = - BluetoothTileDialogDelegate( - BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED), - MATCH_PARENT, - bluetoothTileDialogCallback, - {}, - dispatcher, - fakeSystemClock, - uiEventLogger, - logger, - sysuiDialogFactory, - kosmos.shadeDialogContextInteractor, - ) - .createDialog() - dialog.show() - assertThat( - dialog.requireViewById<View>(R.id.bluetooth_auto_on_toggle_layout).visibility - ) - .isEqualTo(GONE) - dialog.dismiss() - } - } - - @Test - fun testOnAudioSharingButtonUpdated_visibleActive_activateButton() { - testScope.runTest { - val dialog = mBluetoothTileDialogDelegate.createDialog() - dialog.show() - fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE) - mBluetoothTileDialogDelegate.onAudioSharingButtonUpdated( - dialog, - visibility = VISIBLE, - label = null, - isActive = true, - ) - - val audioSharingButton = dialog.requireViewById<View>(R.id.audio_sharing_button) - - assertThat(audioSharingButton).isNotNull() - assertThat(audioSharingButton.visibility).isEqualTo(VISIBLE) - assertThat(audioSharingButton.isActivated).isTrue() - dialog.dismiss() - } - } - - @Test - fun testOnAudioSharingButtonUpdated_visibleNotActive_inactivateButton() { - testScope.runTest { - val dialog = mBluetoothTileDialogDelegate.createDialog() - dialog.show() - fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE) - mBluetoothTileDialogDelegate.onAudioSharingButtonUpdated( - dialog, - visibility = VISIBLE, - label = null, - isActive = false, - ) - - val audioSharingButton = dialog.requireViewById<View>(R.id.audio_sharing_button) - - assertThat(audioSharingButton).isNotNull() - assertThat(audioSharingButton.visibility).isEqualTo(VISIBLE) - assertThat(audioSharingButton.isActivated).isFalse() - dialog.dismiss() - } - } - - @Test - fun testOnAudioSharingButtonUpdated_gone_inactivateButton() { - testScope.runTest { - val dialog = mBluetoothTileDialogDelegate.createDialog() - dialog.show() - fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE) - mBluetoothTileDialogDelegate.onAudioSharingButtonUpdated( - dialog, - visibility = GONE, - label = null, - isActive = false, - ) - - val audioSharingButton = dialog.requireViewById<View>(R.id.audio_sharing_button) - - assertThat(audioSharingButton).isNotNull() - assertThat(audioSharingButton.visibility).isEqualTo(GONE) - assertThat(audioSharingButton.isActivated).isFalse() - dialog.dismiss() - } + verify(bluetoothDetailsContentManager).releaseView() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt index 242d31a26b13..47a834be9b9c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt @@ -76,8 +76,6 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { private lateinit var bluetoothTileDialogViewModel: BluetoothTileDialogViewModel - @Mock private lateinit var bluetoothStateInteractor: BluetoothStateInteractor - @Mock private lateinit var bluetoothDeviceMetadataInteractor: BluetoothDeviceMetadataInteractor @Mock private lateinit var deviceItemInteractor: DeviceItemInteractor @@ -106,9 +104,16 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { @Mock private lateinit var bluetoothTileDialogDelegate: BluetoothTileDialogDelegate + @Mock + private lateinit var bluetoothDetailsContentManagerFactory: + BluetoothDetailsContentManager.Factory + + @Mock private lateinit var bluetoothDetailsContentManager: BluetoothDetailsContentManager + @Mock private lateinit var sysuiDialog: SystemUIDialog @Mock private lateinit var expandable: Expandable @Mock private lateinit var controller: DialogTransitionAnimator.Controller + @Mock private lateinit var mockView: View private val sharedPreferences = FakeSharedPreferences() @@ -129,7 +134,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { localBluetoothManager, bluetoothTileDialogLogger, testScope.backgroundScope, - dispatcher + dispatcher, ), // TODO(b/316822488): Create FakeBluetoothAutoOnInteractor. BluetoothAutoOnInteractor( @@ -137,7 +142,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { localBluetoothManager, bluetoothAdapter, testScope.backgroundScope, - dispatcher + dispatcher, ) ), kosmos.audioSharingInteractor, @@ -151,7 +156,8 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { dispatcher, dispatcher, sharedPreferences, - mBluetoothTileDialogDelegateDelegateFactory + mBluetoothTileDialogDelegateDelegateFactory, + bluetoothDetailsContentManagerFactory, ) whenever(deviceItemInteractor.deviceItemUpdate).thenReturn(MutableSharedFlow()) whenever(deviceItemInteractor.deviceItemUpdateRequest) @@ -161,20 +167,34 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { whenever(mBluetoothTileDialogDelegateDelegateFactory.create(any(), anyInt(), any(), any())) .thenReturn(bluetoothTileDialogDelegate) whenever(bluetoothTileDialogDelegate.createDialog()).thenReturn(sysuiDialog) + whenever(bluetoothTileDialogDelegate.contentManager) + .thenReturn(bluetoothDetailsContentManager) + whenever( + bluetoothDetailsContentManagerFactory.create( + any(), + anyInt(), + any(), + anyBoolean(), + any(), + ) + ) + .thenReturn(bluetoothDetailsContentManager) whenever(sysuiDialog.context).thenReturn(mContext) - whenever(bluetoothTileDialogDelegate.bluetoothStateToggle) + whenever(bluetoothDetailsContentManager.bluetoothStateToggle) .thenReturn(getMutableStateFlow(false)) - whenever(bluetoothTileDialogDelegate.deviceItemClick).thenReturn(MutableSharedFlow()) - whenever(bluetoothTileDialogDelegate.contentHeight).thenReturn(getMutableStateFlow(0)) - whenever(bluetoothTileDialogDelegate.bluetoothAutoOnToggle) + whenever(bluetoothDetailsContentManager.deviceItemClick) + .thenReturn(getMutableStateFlow(null)) + whenever(bluetoothDetailsContentManager.contentHeight).thenReturn(getMutableStateFlow(0)) + whenever(bluetoothDetailsContentManager.bluetoothAutoOnToggle) .thenReturn(getMutableStateFlow(false)) whenever(expandable.dialogTransitionController(any())).thenReturn(controller) + whenever(mockView.context).thenReturn(mContext) } @Test - fun testShowDialog_noAnimation() { + fun testShowDetailsContent_noAnimation() { testScope.runTest { - bluetoothTileDialogViewModel.showDialog(null) + bluetoothTileDialogViewModel.showDetailsContent(null, null) runCurrent() verify(mDialogTransitionAnimator, never()).show(any(), any(), any()) @@ -182,9 +202,9 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { } @Test - fun testShowDialog_animated() { + fun testShowDetailsContent_animated() { testScope.runTest { - bluetoothTileDialogViewModel.showDialog(expandable) + bluetoothTileDialogViewModel.showDetailsContent(expandable, null) runCurrent() verify(mDialogTransitionAnimator).show(any(), any(), anyBoolean()) @@ -192,10 +212,21 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { } @Test - fun testShowDialog_animated_callInBackgroundThread() { + fun testShowDetailsContent_animated_inDetailsView() { + testScope.runTest { + bluetoothTileDialogViewModel.showDetailsContent(expandable, mockView) + runCurrent() + + verify(bluetoothDetailsContentManager).bind(mockView) + verify(bluetoothDetailsContentManager).start() + } + } + + @Test + fun testShowDetailsContent_animated_callInBackgroundThread() { testScope.runTest { backgroundExecutor.execute { - bluetoothTileDialogViewModel.showDialog(expandable) + bluetoothTileDialogViewModel.showDetailsContent(expandable, null) runCurrent() verify(mDialogTransitionAnimator).show(any(), any(), anyBoolean()) @@ -204,9 +235,22 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { } @Test - fun testShowDialog_fetchDeviceItem() { + fun testShowDetailsContent_animated_callInBackgroundThread_inDetailsView() { + testScope.runTest { + backgroundExecutor.execute { + bluetoothTileDialogViewModel.showDetailsContent(expandable, mockView) + runCurrent() + + verify(bluetoothDetailsContentManager).bind(mockView) + verify(bluetoothDetailsContentManager).start() + } + } + } + + @Test + fun testShowDetailsContent_fetchDeviceItem() { testScope.runTest { - bluetoothTileDialogViewModel.showDialog(null) + bluetoothTileDialogViewModel.showDetailsContent(null, null) runCurrent() verify(deviceItemInteractor).deviceItemUpdate @@ -217,7 +261,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { fun testStartSettingsActivity_activityLaunched_dialogDismissed() { testScope.runTest { whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice) - bluetoothTileDialogViewModel.showDialog(null) + bluetoothTileDialogViewModel.showDetailsContent(null, null) runCurrent() val clickedView = View(context) @@ -234,7 +278,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { val actual = BluetoothTileDialogViewModel.UiProperties.build( isBluetoothEnabled = true, - isAutoOnToggleFeatureAvailable = true + isAutoOnToggleFeatureAvailable = true, ) assertThat(actual.autoOnToggleVisibility).isEqualTo(GONE) } @@ -246,7 +290,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { val actual = BluetoothTileDialogViewModel.UiProperties.build( isBluetoothEnabled = false, - isAutoOnToggleFeatureAvailable = true + isAutoOnToggleFeatureAvailable = true, ) assertThat(actual.autoOnToggleVisibility).isEqualTo(VISIBLE) } @@ -258,7 +302,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { val actual = BluetoothTileDialogViewModel.UiProperties.build( isBluetoothEnabled = false, - isAutoOnToggleFeatureAvailable = false + isAutoOnToggleFeatureAvailable = false, ) assertThat(actual.autoOnToggleVisibility).isEqualTo(GONE) } 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/qs/panels/ui/compose/DragAndDropTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt index fc720b836f72..26cf4a261289 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt @@ -68,6 +68,7 @@ class DragAndDropTest : SysuiTestCase() { columns = 4, largeTilesSpan = 4, modifier = Modifier.fillMaxSize(), + onAddTile = {}, onRemoveTile = {}, onSetTiles = onSetTiles, onResize = { _, _ -> }, diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt new file mode 100644 index 000000000000..4e8b0bcd374c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt @@ -0,0 +1,199 @@ +/* + * 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.qs.panels.ui.compose + +import androidx.compose.foundation.layout.fillMaxSize +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.semantics.SemanticsProperties +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.filter +import androidx.compose.ui.test.hasContentDescription +import androidx.compose.ui.test.junit4.ComposeContentTestRule +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onChildren +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.text.AnnotatedString +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.qs.panels.shared.model.SizedTile +import com.android.systemui.qs.panels.shared.model.SizedTileImpl +import com.android.systemui.qs.panels.ui.compose.infinitegrid.DefaultEditTileGrid +import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.shared.model.TileCategory +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class EditModeTest : SysuiTestCase() { + @get:Rule val composeRule = createComposeRule() + + @Composable + private fun EditTileGridUnderTest() { + var tiles by remember { mutableStateOf(TestEditTiles) } + val (currentTiles, otherTiles) = tiles.partition { it.tile.isCurrent } + val listState = EditTileListState(currentTiles, columns = 4, largeTilesSpan = 2) + DefaultEditTileGrid( + listState = listState, + otherTiles = otherTiles, + columns = 4, + largeTilesSpan = 4, + modifier = Modifier.fillMaxSize(), + onAddTile = { tiles = tiles.add(it) }, + onRemoveTile = { tiles = tiles.remove(it) }, + onSetTiles = {}, + onResize = { _, _ -> }, + onStopEditing = {}, + onReset = null, + ) + } + + @Test + fun clickAvailableTile_shouldAdd() { + composeRule.setContent { EditTileGridUnderTest() } + composeRule.waitForIdle() + + composeRule.onNodeWithContentDescription("tileF").performClick() // Tap to add + composeRule.waitForIdle() + + composeRule.assertCurrentTilesGridContainsExactly( + listOf("tileA", "tileB", "tileC", "tileD_large", "tileE", "tileF") + ) + composeRule.assertAvailableTilesGridContainsExactly(listOf("tileG_large")) + } + + @Test + fun clickRemoveTarget_shouldRemoveSelection() { + composeRule.setContent { EditTileGridUnderTest() } + composeRule.waitForIdle() + + composeRule.onNodeWithContentDescription("tileA").performClick() // Selects + composeRule.onNodeWithText("Remove").performClick() // Removes + + composeRule.waitForIdle() + + composeRule.assertCurrentTilesGridContainsExactly( + listOf("tileB", "tileC", "tileD_large", "tileE") + ) + composeRule.assertAvailableTilesGridContainsExactly(listOf("tileA", "tileF", "tileG_large")) + } + + private fun ComposeContentTestRule.assertCurrentTilesGridContainsExactly(specs: List<String>) = + assertGridContainsExactly(CURRENT_TILES_GRID_TEST_TAG, specs) + + private fun ComposeContentTestRule.assertAvailableTilesGridContainsExactly( + specs: List<String> + ) = assertGridContainsExactly(AVAILABLE_TILES_GRID_TEST_TAG, specs) + + private fun ComposeContentTestRule.assertGridContainsExactly( + testTag: String, + specs: List<String>, + ) { + onNodeWithTag(testTag) + .onChildren() + .filter(SemanticsMatcher.keyIsDefined(SemanticsProperties.ContentDescription)) + .apply { + fetchSemanticsNodes().forEachIndexed { index, _ -> + get(index).assert(hasContentDescription(specs[index])) + } + } + } + + companion object { + private const val CURRENT_TILES_GRID_TEST_TAG = "CurrentTilesGrid" + private const val AVAILABLE_TILES_GRID_TEST_TAG = "AvailableTilesGrid" + + private fun List<SizedTile<EditTileViewModel>>.add( + spec: TileSpec + ): List<SizedTile<EditTileViewModel>> { + return map { + if (it.tile.tileSpec == spec) { + createEditTile(it.tile.tileSpec.spec) + } else { + it + } + } + } + + private fun List<SizedTile<EditTileViewModel>>.remove( + spec: TileSpec + ): List<SizedTile<EditTileViewModel>> { + return map { + if (it.tile.tileSpec == spec) { + createEditTile(it.tile.tileSpec.spec, isCurrent = false) + } else { + it + } + } + } + + private fun createEditTile( + tileSpec: String, + isCurrent: Boolean = true, + ): SizedTile<EditTileViewModel> { + return SizedTileImpl( + EditTileViewModel( + tileSpec = TileSpec.create(tileSpec), + icon = + Icon.Resource( + android.R.drawable.star_on, + ContentDescription.Loaded(tileSpec), + ), + label = AnnotatedString(tileSpec), + appName = null, + isCurrent = isCurrent, + availableEditActions = emptySet(), + category = TileCategory.UNKNOWN, + ), + getWidth(tileSpec), + ) + } + + private fun getWidth(tileSpec: String): Int { + return if (tileSpec.endsWith("large")) { + 2 + } else { + 1 + } + } + + private val TestEditTiles = + listOf( + createEditTile("tileA"), + createEditTile("tileB"), + createEditTile("tileC"), + createEditTile("tileD_large"), + createEditTile("tileE"), + createEditTile("tileF", isCurrent = false), + createEditTile("tileG_large", isCurrent = false), + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt index f23553eda3b2..a0be02f1ef7e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt @@ -65,6 +65,7 @@ class ResizingTest : SysuiTestCase() { columns = 4, largeTilesSpan = 4, modifier = Modifier.fillMaxSize(), + onAddTile = {}, onRemoveTile = {}, onSetTiles = {}, onResize = onResize, diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt index 330b887b70a3..1305b0c4c499 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt @@ -238,7 +238,8 @@ class BluetoothTileTest(flags: FlagsParameterization) : SysuiTestCase() { tile.handleClick(null) - verify(bluetoothTileDialogViewModel).showDialog(null) + verify(bluetoothTileDialogViewModel) + .showDetailsContent(/* expandable= */ null, /* view= */ null) } @Test 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/plugins/statusbar/StatusBarStateControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt index 2e6d8ed5aa5b..4f024d7509ba 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt @@ -19,7 +19,6 @@ package com.android.systemui.plugins.statusbar import com.android.internal.logging.uiEventLogger import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor -import com.android.systemui.jank.interactionJankMonitor import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor @@ -37,7 +36,6 @@ var Kosmos.statusBarStateController: SysuiStatusBarStateController by Kosmos.Fixture { StatusBarStateControllerImpl( uiEventLogger, - { interactionJankMonitor }, mock(), { keyguardInteractor }, { keyguardTransitionInteractor }, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt index 65e580cafcb5..583a9def8094 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt @@ -33,6 +33,7 @@ import com.android.systemui.qs.footerActionsViewModelFactory import com.android.systemui.qs.panels.domain.interactor.tileSquishinessInteractor import com.android.systemui.qs.panels.ui.viewmodel.inFirstPageViewModel import com.android.systemui.qs.panels.ui.viewmodel.mediaInRowInLandscapeViewModelFactory +import com.android.systemui.qs.panels.ui.viewmodel.quickQuickSettingsViewModelFactory import com.android.systemui.qs.ui.viewmodel.quickSettingsContainerViewModelFactory import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.largeScreenHeaderHelper @@ -48,6 +49,7 @@ val Kosmos.qsFragmentComposeViewModelFactory by ): QSFragmentComposeViewModel { return QSFragmentComposeViewModel( quickSettingsContainerViewModelFactory, + quickQuickSettingsViewModelFactory, mainResources, footerActionsViewModelFactory, footerActionsController, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelKosmos.kt index f8fa5db4ddf7..3fc73cbc5552 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelKosmos.kt @@ -20,9 +20,9 @@ import com.android.systemui.brightness.ui.viewmodel.brightnessSliderViewModelFac import com.android.systemui.kosmos.Kosmos import com.android.systemui.qs.panels.ui.viewmodel.detailsViewModel import com.android.systemui.qs.panels.ui.viewmodel.editModeViewModel -import com.android.systemui.qs.panels.ui.viewmodel.quickQuickSettingsViewModelFactory import com.android.systemui.qs.panels.ui.viewmodel.tileGridViewModel import com.android.systemui.qs.panels.ui.viewmodel.toolbar.toolbarViewModelFactory +import com.android.systemui.shade.domain.interactor.shadeModeInteractor import com.android.systemui.shade.ui.viewmodel.shadeHeaderViewModelFactory val Kosmos.quickSettingsContainerViewModelFactory by @@ -33,13 +33,13 @@ val Kosmos.quickSettingsContainerViewModelFactory by ): QuickSettingsContainerViewModel { return QuickSettingsContainerViewModel( brightnessSliderViewModelFactory, - quickQuickSettingsViewModelFactory, shadeHeaderViewModelFactory, supportsBrightnessMirroring, tileGridViewModel, editModeViewModel, detailsViewModel, toolbarViewModelFactory, + shadeModeInteractor, ) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt index 4f3b8f3541e1..108a20ab73d6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt @@ -19,7 +19,6 @@ package com.android.systemui.qs.ui.viewmodel import com.android.systemui.kosmos.Kosmos import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor -import com.android.systemui.shade.ui.viewmodel.shadeHeaderViewModelFactory import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor val Kosmos.quickSettingsShadeOverlayContentViewModel: QuickSettingsShadeOverlayContentViewModel by @@ -28,7 +27,5 @@ val Kosmos.quickSettingsShadeOverlayContentViewModel: QuickSettingsShadeOverlayC shadeInteractor = shadeInteractor, sceneInteractor = sceneInteractor, notificationStackAppearanceInteractor = notificationStackAppearanceInteractor, - shadeHeaderViewModelFactory = shadeHeaderViewModelFactory, - quickSettingsContainerViewModelFactory = quickSettingsContainerViewModelFactory, ) } 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/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt index 7eb9f3472482..2be8acb845b9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.shade.ui.viewmodel import android.content.applicationContext +import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.kosmos.Kosmos import com.android.systemui.plugins.activityStarter @@ -24,8 +25,12 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.domain.interactor.privacyChipInteractor import com.android.systemui.shade.domain.interactor.shadeHeaderClockInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder +import com.android.systemui.statusbar.phone.ui.StatusBarIconController +import com.android.systemui.statusbar.phone.ui.TintedIconManager import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.mobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.mobileIconsViewModel +import org.mockito.kotlin.mock val Kosmos.shadeHeaderViewModel: ShadeHeaderViewModel by Kosmos.Fixture { @@ -38,6 +43,11 @@ val Kosmos.shadeHeaderViewModel: ShadeHeaderViewModel by mobileIconsViewModel = mobileIconsViewModel, privacyChipInteractor = privacyChipInteractor, clockInteractor = shadeHeaderClockInteractor, + tintedIconManagerFactory = mock<TintedIconManager.Factory>(), + batteryMeterViewControllerFactory = mock<BatteryMeterViewController.Factory>(), + statusBarIconController = mock<StatusBarIconController>(), + notificationIconContainerStatusBarViewBinder = + mock<NotificationIconContainerStatusBarViewBinder>(), broadcastDispatcher = broadcastDispatcher, ) } diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java index 3cb6c5a6bd16..7af03ed2e6c8 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java @@ -44,6 +44,7 @@ import android.app.UiAutomation; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.res.Resources; +import android.icu.util.ULocale; import android.os.Binder; import android.os.Build; import android.os.Build.VERSION_CODES; @@ -81,6 +82,7 @@ import java.io.IOException; import java.io.PrintStream; import java.util.Collections; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Random; @@ -228,6 +230,9 @@ public class RavenwoodRuntimeEnvironmentController { RuntimeInit.redirectLogStreams(); dumpCommandLineArgs(); + dumpEnvironment(); + dumpJavaProperties(); + dumpOtherInfo(); // We haven't initialized liblog yet, so directly write to System.out here. RavenwoodCommonUtils.log(TAG, "globalInitInner()"); @@ -564,4 +569,34 @@ public class RavenwoodRuntimeEnvironmentController { Log.v(TAG, " " + arg); } } + + private static void dumpJavaProperties() { + Log.v(TAG, "JVM properties:"); + dumpMap(System.getProperties()); + } + + private static void dumpEnvironment() { + Log.v(TAG, "Environment:"); + dumpMap(System.getenv()); + } + + private static void dumpMap(Map<?, ?> map) { + for (var key : map.keySet().stream().sorted().toList()) { + Log.v(TAG, " " + key + "=" + map.get(key)); + } + } + + private static void dumpOtherInfo() { + Log.v(TAG, "Other key information:"); + var jloc = Locale.getDefault(); + Log.v(TAG, " java.util.Locale=" + jloc + " / " + jloc.toLanguageTag()); + var uloc = ULocale.getDefault(); + Log.v(TAG, " android.icu.util.ULocale=" + uloc + " / " + uloc.toLanguageTag()); + + var jtz = java.util.TimeZone.getDefault(); + Log.v(TAG, " java.util.TimeZone=" + jtz.getDisplayName() + " / " + jtz); + + var itz = android.icu.util.TimeZone.getDefault(); + Log.v(TAG, " android.icu.util.TimeZone=" + itz.getDisplayName() + " / " + itz); + } } diff --git a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java index df47c98d6433..89c9d690a82c 100644 --- a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java +++ b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java @@ -27,10 +27,6 @@ import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE; import static android.content.pm.PackageManager.PERMISSION_GRANTED; -import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR; -import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; -import static android.view.WindowManager.LayoutParams.TYPE_POINTER; -import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR; import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_CONTENT; import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_STRUCTURE; @@ -74,6 +70,7 @@ import android.util.Log; import android.util.Slog; import android.view.IWindowManager; import android.window.ScreenCapture; +import android.window.ScreenCapture.ScreenshotHardwareBuffer; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; @@ -333,10 +330,10 @@ public class ContextualSearchManagerService extends SystemService { isManagedProfileVisible = true; } } + final String csPackage = Objects.requireNonNull(launchIntent.getPackage()); + final int csUid = mPackageManager.getPackageUid(csPackage, /* flags */ 0L, userId); if (isAssistDataAllowed) { try { - final String csPackage = Objects.requireNonNull(launchIntent.getPackage()); - final int csUid = mPackageManager.getPackageUid(csPackage, 0, 0); mAssistDataRequester.requestAssistData( activityTokens, /* fetchData */ true, @@ -350,17 +347,8 @@ public class ContextualSearchManagerService extends SystemService { Log.e(TAG, "Could not request assist data", e); } } - final ScreenCapture.ScreenshotHardwareBuffer shb; - if (mWmInternal != null) { - shb = mWmInternal.takeAssistScreenshot(Set.of( - TYPE_STATUS_BAR, - TYPE_NAVIGATION_BAR, - TYPE_NAVIGATION_BAR_PANEL, - TYPE_POINTER)); - } else { - if (DEBUG) Log.w(TAG, "Can't capture contextual screenshot: mWmInternal is null"); - shb = null; - } + final ScreenshotHardwareBuffer shb = mWmInternal.takeContextualSearchScreenshot( + (Flags.contextualSearchWindowLayer() ? csUid : -1)); final Bitmap bm = shb != null ? shb.asBitmap() : null; // Now that everything is fetched, putting it in the launchIntent. if (bm != null) { @@ -509,15 +497,17 @@ public class ContextualSearchManagerService extends SystemService { bundle.putParcelable(ContextualSearchManager.EXTRA_TOKEN, mToken); // We get take the screenshot with the system server's identity because the system // server has READ_FRAME_BUFFER permission to get the screenshot. + final int callingUid = Binder.getCallingUid(); Binder.withCleanCallingIdentity(() -> { - if (mWmInternal != null) { + final ScreenshotHardwareBuffer shb = + mWmInternal.takeContextualSearchScreenshot( + (Flags.contextualSearchWindowLayer() ? callingUid : -1)); + final Bitmap bm = shb != null ? shb.asBitmap() : null; + if (bm != null) { bundle.putParcelable(ContextualSearchManager.EXTRA_SCREENSHOT, - mWmInternal.takeAssistScreenshot(Set.of( - TYPE_STATUS_BAR, - TYPE_NAVIGATION_BAR, - TYPE_NAVIGATION_BAR_PANEL, - TYPE_POINTER)) - .asBitmap().asShared()); + bm.asShared()); + bundle.putBoolean(ContextualSearchManager.EXTRA_FLAG_SECURE_FOUND, + shb.containsSecureLayers()); } try { callback.onResult( diff --git a/services/core/java/com/android/server/BootReceiver.java b/services/core/java/com/android/server/BootReceiver.java index 7a5b8660ef7c..cf0d7e72ba8b 100644 --- a/services/core/java/com/android/server/BootReceiver.java +++ b/services/core/java/com/android/server/BootReceiver.java @@ -53,6 +53,7 @@ import com.android.modules.utils.TypedXmlSerializer; import com.android.server.am.DropboxRateLimiter; import com.android.server.os.TombstoneProtos.Tombstone; +import libcore.io.IoUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -154,6 +155,10 @@ public class BootReceiver extends BroadcastReceiver { @Override public void onReceive(final Context context, Intent intent) { + if (!Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { + return; + } + // Log boot events in the background to avoid blocking the main thread with I/O new Thread() { @Override @@ -219,6 +224,8 @@ public class BootReceiver extends BroadcastReceiver { } catch (Exception e) { Slog.wtf(TAG, "Error watching for trace events", e); return 0; // Unregister the handler. + } finally { + IoUtils.closeQuietly(fd); } return OnFileDescriptorEventListener.EVENT_INPUT; } 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 5184a2c4ec30..c6338307b192 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -186,8 +186,10 @@ public class SettingsToPropertiesMapper { "core_libraries", "crumpet", "dck_framework", + "desktop_connectivity", "desktop_hwsec", "desktop_stats", + "desktop_wifi", "devoptions_settings", "game", "gpu", @@ -218,6 +220,7 @@ public class SettingsToPropertiesMapper { "pixel_continuity", "pixel_display", "pixel_perf", + "pixel_sensai", "pixel_sensors", "pixel_state_server", "pixel_system_sw_video", 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/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/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 21ac05c24259..638b2dd4a7fe 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -111,7 +111,9 @@ import static android.os.UserHandle.USER_SYSTEM; import static android.service.notification.Adjustment.KEY_SUMMARIZATION; import static android.service.notification.Adjustment.KEY_TYPE; import static android.service.notification.Adjustment.TYPE_CONTENT_RECOMMENDATION; +import static android.service.notification.Adjustment.TYPE_NEWS; import static android.service.notification.Adjustment.TYPE_PROMOTION; +import static android.service.notification.Adjustment.TYPE_SOCIAL_MEDIA; import static android.service.notification.Flags.FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT; import static android.service.notification.Flags.callstyleCallbackApi; import static android.service.notification.Flags.notificationClassification; @@ -492,7 +494,10 @@ public class NotificationManagerService extends SystemService { }; static final Integer[] DEFAULT_ALLOWED_ADJUSTMENT_KEY_TYPES = new Integer[] { - TYPE_PROMOTION + TYPE_PROMOTION, + TYPE_NEWS, + TYPE_CONTENT_RECOMMENDATION, + TYPE_SOCIAL_MEDIA }; static final String[] NON_BLOCKABLE_DEFAULT_ROLES = new String[] { @@ -4522,7 +4527,7 @@ public class NotificationManagerService extends SystemService { if (key == null) { return; } - mAssistants.setAdjustmentTypeSupportedState(info, key, supported); + mAssistants.setAdjustmentTypeSupportedState(info.userid, key, supported); } } finally { Binder.restoreCallingIdentity(identity); @@ -4565,6 +4570,12 @@ public class NotificationManagerService extends SystemService { } @Override + public String[] getAdjustmentDeniedPackages(String key) { + checkCallerIsSystemOrSystemUiOrShell(); + return mAssistants.getAdjustmentDeniedPackages(key); + } + + @Override public boolean isAdjustmentSupportedForPackage(String key, String pkg) { checkCallerIsSystemOrSystemUiOrShell(); return mAssistants.isAdjustmentAllowedForPackage(key, pkg); @@ -7017,7 +7028,7 @@ public class NotificationManagerService extends SystemService { final long identity = Binder.clearCallingIdentity(); try { synchronized (mNotificationLock) { - mAssistants.checkServiceTokenLocked(token); + ManagedServiceInfo info = mAssistants.checkServiceTokenLocked(token); int N = mEnqueuedNotifications.size(); for (int i = 0; i < N; i++) { final NotificationRecord r = mEnqueuedNotifications.get(i); @@ -7363,6 +7374,10 @@ public class NotificationManagerService extends SystemService { mAssistants.setPackageOrComponentEnabled(assistant.flattenToString(), userId, true, granted, userSet); + if (android.service.notification.Flags.notificationClassification()) { + mAssistants.setNasUnsupportedDefaults(userId); + } + getContext().sendBroadcastAsUser( new Intent(ACTION_NOTIFICATION_POLICY_ACCESS_GRANTED_CHANGED) .setPackage(assistant.getPackageName()) @@ -7394,7 +7409,8 @@ public class NotificationManagerService extends SystemService { toRemove.add(potentialKey); } if (notificationClassification() && potentialKey.equals(KEY_TYPE)) { - mAssistants.setNasUnsupportedDefaults(r.getSbn().getNormalizedUserId()); + mAssistants.setAdjustmentTypeSupportedState( + r.getSbn().getNormalizedUserId(), potentialKey, true); if (!mAssistants.isAdjustmentKeyTypeAllowed(adjustments.getInt(KEY_TYPE))) { toRemove.add(potentialKey); } else if (notificationClassificationUi() @@ -7405,6 +7421,8 @@ public class NotificationManagerService extends SystemService { } if ((nmSummarization() || nmSummarizationUi()) && potentialKey.equals(KEY_SUMMARIZATION)) { + mAssistants.setAdjustmentTypeSupportedState( + r.getSbn().getNormalizedUserId(), potentialKey, true); if (!mAssistants.isAdjustmentAllowedForPackage(KEY_SUMMARIZATION, r.getSbn().getPackageName())) { toRemove.add(potentialKey); @@ -12062,8 +12080,8 @@ public class NotificationManagerService extends SystemService { private static final String TAG_DENIED_KEY = "adjustment"; private static final String ATT_DENIED_KEY = "key"; private static final String ATT_DENIED_KEY_APPS = "denied_apps"; - private static final String TAG_ENABLED_TYPES = "enabled_key_types"; - private static final String ATT_NAS_UNSUPPORTED = "nas_unsupported_adjustments"; + private static final String TAG_ENABLED_TYPES = "enabled_classification_types"; + private static final String ATT_NAS_UNSUPPORTED = "unsupported_adjustments"; private final Object mLock = new Object(); @@ -12283,6 +12301,16 @@ public class NotificationManagerService extends SystemService { } } + protected @NonNull String[] getAdjustmentDeniedPackages(@Adjustment.Keys String key) { + synchronized (mLock) { + if (notificationClassificationUi() || nmSummarization() | nmSummarizationUi()) { + return mAdjustmentKeyDeniedPackages.getOrDefault( + key, new ArraySet<>()).toArray(new String[0]); + } + } + return new String[]{}; + } + protected @NonNull boolean isAdjustmentAllowedForPackage(@Adjustment.Keys String key, String pkg) { synchronized (mLock) { @@ -12665,10 +12693,6 @@ public class NotificationManagerService extends SystemService { setNotificationAssistantAccessGrantedForUserInternal( currentComponent, userId, false, userSet); } - } else { - if (android.service.notification.Flags.notificationClassification()) { - setNasUnsupportedDefaults(userId); - } } super.setPackageOrComponentEnabled(pkgOrComponent, userId, isPrimary, enabled, userSet); } @@ -12701,36 +12725,37 @@ public class NotificationManagerService extends SystemService { } } - @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION) @GuardedBy("mNotificationLock") - public void setAdjustmentTypeSupportedState(ManagedServiceInfo info, + public void setAdjustmentTypeSupportedState(@UserIdInt int userId, @Adjustment.Keys String key, boolean supported) { - if (!android.service.notification.Flags.notificationClassification()) { + if (!(android.service.notification.Flags.notificationClassification() + || android.app.Flags.nmSummarizationUi() + || android.app.Flags.nmSummarization())) { return; } - setNasUnsupportedDefaults(info.userid); - HashSet<String> disabledAdjustments = mNasUnsupported.get(info.userid); + HashSet<String> disabledAdjustments = + mNasUnsupported.getOrDefault(userId, new HashSet<>()); if (supported) { disabledAdjustments.remove(key); } else { disabledAdjustments.add(key); } - mNasUnsupported.put(info.userid, disabledAdjustments); + mNasUnsupported.put(userId, disabledAdjustments); handleSavePolicyFile(); } - @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION) @GuardedBy("mNotificationLock") public @NonNull Set<String> getUnsupportedAdjustments(@UserIdInt int userId) { - if (!android.service.notification.Flags.notificationClassification()) { + if (!(android.service.notification.Flags.notificationClassification() + || android.app.Flags.nmSummarizationUi() + || android.app.Flags.nmSummarization())) { return new HashSet<>(); } - setNasUnsupportedDefaults(userId); - return mNasUnsupported.get(userId); + return mNasUnsupported.getOrDefault(userId, new HashSet<>()); } private void setNasUnsupportedDefaults(@UserIdInt int userId) { - if (mNasUnsupported != null && !mNasUnsupported.containsKey(userId)) { + if (mNasUnsupported != null) { mNasUnsupported.put(userId, new HashSet(List.of(mDefaultUnsupportedAdjustments))); handleSavePolicyFile(); } @@ -12743,10 +12768,8 @@ public class NotificationManagerService extends SystemService { return; } synchronized (mLock) { - if (mNasUnsupported.containsKey(approvedUserId)) { - out.attribute(null, ATT_NAS_UNSUPPORTED, - TextUtils.join(",", mNasUnsupported.get(approvedUserId))); - } + out.attribute(null, ATT_NAS_UNSUPPORTED, TextUtils.join(",", + mNasUnsupported.getOrDefault(approvedUserId, new HashSet<>()))); } } @@ -12759,8 +12782,15 @@ public class NotificationManagerService extends SystemService { if (ManagedServices.TAG_MANAGED_SERVICES.equals(tag)) { final String types = XmlUtils.readStringAttribute(parser, ATT_NAS_UNSUPPORTED); synchronized (mLock) { - if (!TextUtils.isEmpty(types)) { - mNasUnsupported.put(approvedUserId, new HashSet(List.of(types.split(",")))); + if (types == null) { + setNasUnsupportedDefaults(approvedUserId); + } else { + if (!TextUtils.isEmpty(types)) { + mNasUnsupported.put(approvedUserId, + new HashSet(List.of(types.split(",")))); + } else { + mNasUnsupported.put(approvedUserId, new HashSet()); + } } } } diff --git a/services/core/java/com/android/server/pm/UserManagerInternal.java b/services/core/java/com/android/server/pm/UserManagerInternal.java index 4d97a83fc6b4..f88681dbcaeb 100644 --- a/services/core/java/com/android/server/pm/UserManagerInternal.java +++ b/services/core/java/com/android/server/pm/UserManagerInternal.java @@ -353,8 +353,10 @@ public abstract class UserManagerInternal { boolean excludePreCreated); /** - * Returns an array of ids for profiles associated with the specified user including the user - * itself. + * Returns a list of the users that are associated with the specified user, including the user + * itself. This includes the user, its profiles, its parent, and its parent's other profiles, + * as applicable. + * * <p>Note that this includes all profile types (not including Restricted profiles). * * @param userId id of the user to return profiles for diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 0a90c3644c2f..7de7dde8c260 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -1636,7 +1636,7 @@ public class UserManagerService extends IUserManager.Stub { final int userSize = mUsers.size(); for (int i = 0; i < userSize; i++) { UserInfo profile = mUsers.valueAt(i).info; - if (!isProfileOf(user, profile)) { + if (!isSameProfileGroup(user, profile)) { continue; } if (enabledOnly && !profile.isEnabled()) { @@ -1704,22 +1704,18 @@ public class UserManagerService extends IUserManager.Stub { return isSameProfileGroupNoChecks(userId, otherUserId); } - /** - * Returns whether users are in the same non-empty profile group. - * Currently, false if empty profile group, even if they are the same user, for whatever reason. - */ + /** Returns whether users are in the same profile group. */ private boolean isSameProfileGroupNoChecks(@UserIdInt int userId, int otherUserId) { synchronized (mUsersLock) { UserInfo userInfo = getUserInfoLU(userId); - if (userInfo == null || userInfo.profileGroupId == UserInfo.NO_PROFILE_GROUP_ID) { + if (userInfo == null) { return false; } UserInfo otherUserInfo = getUserInfoLU(otherUserId); - if (otherUserInfo == null - || otherUserInfo.profileGroupId == UserInfo.NO_PROFILE_GROUP_ID) { + if (otherUserInfo == null) { return false; } - return userInfo.profileGroupId == otherUserInfo.profileGroupId; + return isSameProfileGroup(userInfo, otherUserInfo); } } @@ -1778,10 +1774,10 @@ public class UserManagerService extends IUserManager.Stub { } } - private static boolean isProfileOf(UserInfo user, UserInfo profile) { - return user.id == profile.id || - (user.profileGroupId != UserInfo.NO_PROFILE_GROUP_ID - && user.profileGroupId == profile.profileGroupId); + private static boolean isSameProfileGroup(@NonNull UserInfo user1, @NonNull UserInfo user2) { + return user1.id == user2.id || + (user1.profileGroupId != UserInfo.NO_PROFILE_GROUP_ID + && user1.profileGroupId == user2.profileGroupId); } private String getAvailabilityIntentAction(boolean enableQuietMode, boolean useManagedActions) { @@ -7549,6 +7545,10 @@ public class UserManagerService extends IUserManager.Stub { pw.println(" (and being updated after boot)"); } } + if (isHeadlessSystemUserMode) { + pw.println(" Can switch to headless system user: " + Resources.getSystem() + .getBoolean(com.android.internal.R.bool.config_canSwitchToHeadlessSystemUser)); + } pw.println(" User version: " + mUserVersion); pw.println(" Owner name: " + getOwnerName()); if (DBG_ALLOCATION) { @@ -8411,21 +8411,27 @@ public class UserManagerService extends IUserManager.Stub { } /** - * Checks if the given user has a profile associated with it. - * @param userId The parent user - * @return + * Formerly: Checks if the given user has a profile associated with it. + * Now: Just throws. Do not use it. + * @param userId The parent user (passing in a profile user is not supported) + * @deprecated */ boolean hasProfile(@UserIdInt int userId) { - synchronized (mUsersLock) { - UserInfo userInfo = getUserInfoLU(userId); - final int userSize = mUsers.size(); - for (int i = 0; i < userSize; i++) { - UserInfo profile = mUsers.valueAt(i).info; - if (userId != profile.id && isProfileOf(userInfo, profile)) { - return true; + if (!android.content.pm.Flags.removeCrossUserPermissionHack()) { + synchronized (mUsersLock) { + UserInfo userInfo = getUserInfoLU(userId); + final int userSize = mUsers.size(); + for (int i = 0; i < userSize; i++) { + UserInfo profile = mUsers.valueAt(i).info; + if (userId != profile.id && isSameProfileGroup(userInfo, profile)) { + return true; + } } + return false; } - return false; + } else { + // TODO(b/332664521): Remove this method entirely. It is no longer used. + throw new UnsupportedOperationException(); } } diff --git a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java index 2bc6d53147fb..a1082481abb8 100644 --- a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java +++ b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java @@ -309,7 +309,8 @@ public class UserRestrictionsUtils { * in settings. So it is handled separately. */ private static final Set<String> DEFAULT_ENABLED_FOR_MANAGED_PROFILES = Sets.newArraySet( - UserManager.DISALLOW_BLUETOOTH_SHARING + UserManager.DISALLOW_BLUETOOTH_SHARING, + UserManager.DISALLOW_DEBUGGING_FEATURES ); /** 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/power/stats/WakelockStatsFrameworkEvents.java b/services/core/java/com/android/server/power/stats/WakelockStatsFrameworkEvents.java index f387feca05f2..a2971f302327 100644 --- a/services/core/java/com/android/server/power/stats/WakelockStatsFrameworkEvents.java +++ b/services/core/java/com/android/server/power/stats/WakelockStatsFrameworkEvents.java @@ -272,14 +272,10 @@ public class WakelockStatsFrameworkEvents { WakeLockStats extraTime = openOverflowStats.computeIfAbsent(key, k -> new WakeLockStats()); - stats.uptimeMillis += openWakeLockUptime + extraTime.uptimeMillis; - - logger.logResult( - key.getUid(), - key.getTag(), - key.getPowerManagerWakeLockLevel(), - stats.uptimeMillis, - stats.completedCount); + long totalUpdate = openWakeLockUptime + stats.uptimeMillis + extraTime.uptimeMillis; + long totalCount = stats.completedCount + extraTime.completedCount; + logger.logResult(key.getUid(), key.getTag(), key.getPowerManagerWakeLockLevel(), + totalUpdate, totalCount); } } } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index e8498bca1809..0024a4166e71 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -5605,7 +5605,6 @@ final class ActivityRecord extends WindowToken { } mAtmService.mBackNavigationController.onAppVisibilityChanged(this, visible); - onChildVisibilityRequested(visible); final DisplayContent displayContent = getDisplayContent(); displayContent.mOpeningApps.remove(this); 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/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java index e3906f9119c2..0eea30a29580 100644 --- a/services/core/java/com/android/server/wm/DesktopModeHelper.java +++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java @@ -52,8 +52,7 @@ public final class DesktopModeHelper { * Return {@code true} if the current device supports desktop mode. */ // TODO(b/337819319): use a companion object instead. - @VisibleForTesting - static boolean isDesktopModeSupported(@NonNull Context context) { + private static boolean isDesktopModeSupported(@NonNull Context context) { return context.getResources().getBoolean(R.bool.config_isDesktopModeSupported); } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index a0d2d260b39e..ecf2787a2080 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -5165,7 +5165,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp /** * Creates a LayerCaptureArgs object to represent the entire DisplayContent */ - LayerCaptureArgs getLayerCaptureArgs(Set<Integer> windowTypesToExclude) { + LayerCaptureArgs getLayerCaptureArgs(@Nullable ToBooleanFunction<WindowState> predicate) { if (!mWmService.mPolicy.isScreenOn()) { if (DEBUG_SCREENSHOT) { Slog.i(TAG_WM, "Attempted to take screenshot while display was off."); @@ -5178,17 +5178,16 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp LayerCaptureArgs.Builder builder = new LayerCaptureArgs.Builder(getSurfaceControl()) .setSourceCrop(mTmpRect); - if (!windowTypesToExclude.isEmpty()) { - ArrayList<SurfaceControl> surfaceControls = new ArrayList<>(); + if (predicate != null) { + ArrayList<SurfaceControl> excludeLayers = new ArrayList<>(); forAllWindows( window -> { - if (windowTypesToExclude.contains(window.getWindowType())) { - surfaceControls.add(window.mSurfaceControl); + if (!predicate.apply(window)) { + excludeLayers.add(window.mSurfaceControl); } - }, true - ); - if (!surfaceControls.isEmpty()) { - builder.setExcludeLayers(surfaceControls.toArray(new SurfaceControl[0])); + }, true); + if (!excludeLayers.isEmpty()) { + builder.setExcludeLayers(excludeLayers.toArray(new SurfaceControl[0])); } } return builder.build(); 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/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index c77b1d9a7bcf..6e224f07fcdc 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -1137,6 +1137,15 @@ public abstract class WindowManagerInternal { * Returns an instance of {@link ScreenshotHardwareBuffer} containing the current * screenshot. */ - public abstract ScreenshotHardwareBuffer takeAssistScreenshot( - Set<Integer> windowTypesToExclude); + public abstract ScreenshotHardwareBuffer takeAssistScreenshot(); + + /** + * Returns an instance of {@link ScreenshotHardwareBuffer} containing the current + * screenshot, excluding layers that are not appropriate to pass to contextual search + * services - such as the cursor or any current contextual search window. + * + * @param uid the UID of the contextual search application. System alert windows belonging + * to this UID will be excluded from the screenshot. + */ + public abstract ScreenshotHardwareBuffer takeContextualSearchScreenshot(int uid); } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 3a1d652f82d4..7f1924005b2f 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -325,6 +325,7 @@ import android.window.WindowContainerToken; import android.window.WindowContextInfo; import com.android.internal.R; +import com.android.internal.util.ToBooleanFunction; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting.Visibility; @@ -4159,7 +4160,8 @@ public class WindowManagerService extends IWindowManager.Stub } @Nullable - private ScreenshotHardwareBuffer takeAssistScreenshot(Set<Integer> windowTypesToExclude) { + private ScreenshotHardwareBuffer takeAssistScreenshot( + @Nullable ToBooleanFunction<WindowState> predicate) { if (!checkCallingPermission(READ_FRAME_BUFFER, "requestAssistScreenshot()")) { throw new SecurityException("Requires READ_FRAME_BUFFER permission"); } @@ -4174,7 +4176,7 @@ public class WindowManagerService extends IWindowManager.Stub } captureArgs = null; } else { - captureArgs = displayContent.getLayerCaptureArgs(windowTypesToExclude); + captureArgs = displayContent.getLayerCaptureArgs(predicate); } } @@ -4204,8 +4206,7 @@ public class WindowManagerService extends IWindowManager.Stub */ @Override public boolean requestAssistScreenshot(final IAssistDataReceiver receiver) { - final ScreenshotHardwareBuffer shb = - takeAssistScreenshot(/* windowTypesToExclude= */ Set.of()); + final ScreenshotHardwareBuffer shb = takeAssistScreenshot(/* predicate= */ null); final Bitmap bm = shb != null ? shb.asBitmap() : null; FgThread.getHandler().post(() -> { try { @@ -8618,9 +8619,27 @@ public class WindowManagerService extends IWindowManager.Stub } @Override - public ScreenshotHardwareBuffer takeAssistScreenshot(Set<Integer> windowTypesToExclude) { + public ScreenshotHardwareBuffer takeAssistScreenshot() { // WMS.takeAssistScreenshot takes care of the locking. - return WindowManagerService.this.takeAssistScreenshot(windowTypesToExclude); + return WindowManagerService.this.takeAssistScreenshot(/* predicate */ null); + } + + @Override + public ScreenshotHardwareBuffer takeContextualSearchScreenshot(int uid) { + // WMS.takeAssistScreenshot takes care of the locking. + return WindowManagerService.this.takeAssistScreenshot(win -> { + switch (win.getWindowType()) { + case LayoutParams.TYPE_STATUS_BAR: + case LayoutParams.TYPE_NAVIGATION_BAR: + case LayoutParams.TYPE_NAVIGATION_BAR_PANEL: + case LayoutParams.TYPE_POINTER: + return false; + case LayoutParams.TYPE_APPLICATION_OVERLAY: + return uid != win.getOwningUid(); + default: + return true; + } + }); } } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 582cd4ed8003..e11c31c88c87 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -2723,16 +2723,16 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } } - /** - * Apply default restrictions that haven't been applied to a given admin yet. - */ + /** Apply default restrictions that haven't been applied to a given admin yet. */ private void maybeSetDefaultRestrictionsForAdminLocked(int userId, ActiveAdmin admin) { - Set<String> defaultRestrictions = - UserRestrictionsUtils.getDefaultEnabledForManagedProfiles(); - if (defaultRestrictions.equals(admin.defaultEnabledRestrictionsAlreadySet)) { + Set<String> newDefaultRestrictions = new HashSet( + UserRestrictionsUtils.getDefaultEnabledForManagedProfiles()); + newDefaultRestrictions.removeAll(admin.defaultEnabledRestrictionsAlreadySet); + if (newDefaultRestrictions.isEmpty()) { return; // The same set of default restrictions has been already applied. } - for (String restriction : defaultRestrictions) { + + for (String restriction : newDefaultRestrictions) { mDevicePolicyEngine.setLocalPolicy( PolicyDefinition.getPolicyDefinitionForUserRestriction(restriction), EnforcingAdmin.createEnterpriseEnforcingAdmin( @@ -2740,10 +2740,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { admin.getUserHandle().getIdentifier()), new BooleanPolicyValue(true), userId); + admin.defaultEnabledRestrictionsAlreadySet.add(restriction); + Slogf.i(LOG_TAG, "Enabled the following restriction by default: " + restriction); } - admin.defaultEnabledRestrictionsAlreadySet.addAll(defaultRestrictions); - Slogf.i(LOG_TAG, "Enabled the following restrictions by default: " - + defaultRestrictions); } private void maybeStartSecurityLogMonitorOnActivityManagerReady() { @@ -10282,7 +10281,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return false; } - if (isAdb(caller)) { + boolean isAdb = isAdb(caller); + if (isAdb) { // Log profile owner provisioning was started using adb. MetricsLogger.action(mContext, PROVISIONING_ENTRY_POINT_ADB, LOG_TAG_PROFILE_OWNER); DevicePolicyEventLogger @@ -10305,6 +10305,18 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { ensureUnknownSourcesRestrictionForProfileOwnerLocked(userHandle, admin, true /* newOwner */); } + if(isAdb) { + // DISALLOW_DEBUGGING_FEATURES is being added to newly-created + // work profile by default due to b/382064697 . This would have + // impacted certain CTS test flows when they interact with the + // work profile via ADB (for example installing an app into the + // work profile). Remove DISALLOW_DEBUGGING_FEATURES here to + // reduce the potential impact. + setLocalUserRestrictionInternal( + EnforcingAdmin.createEnterpriseEnforcingAdmin(who, userHandle), + UserManager.DISALLOW_DEBUGGING_FEATURES, false, userHandle); + } + sendOwnerChangedBroadcast(DevicePolicyManager.ACTION_PROFILE_OWNER_CHANGED, userHandle); }); 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/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/powerstatstests/src/com/android/server/power/stats/WakelockStatsFrameworkEventsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockStatsFrameworkEventsTest.java index 1fe3f58a9dcb..f24e9ef03398 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockStatsFrameworkEventsTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockStatsFrameworkEventsTest.java @@ -186,14 +186,16 @@ public class WakelockStatsFrameworkEventsTest { public void wakelockOpen() throws Exception { mEvents.noteStartWakeLock(UID_1, TAG_1, WAKELOCK_TYPE_1, TS_1); - ArrayList<WakelockInfo> info = pullResults(TS_3); - - assertEquals("size", 1, info.size()); - assertEquals("uid", UID_1, info.get(0).uid); - assertEquals("tag", TAG_1, info.get(0).tag); - assertEquals("type", WAKELOCK_TYPE_1, info.get(0).type); - assertEquals("duration", TS_3 - TS_1, info.get(0).uptimeMillis); - assertEquals("count", 0, info.get(0).completedCount); + for (int i = 0; i < 5; i++) { + ArrayList<WakelockInfo> info = pullResults(TS_3); + + assertEquals("size", 1, info.size()); + assertEquals("uid", UID_1, info.get(0).uid); + assertEquals("tag", TAG_1, info.get(0).tag); + assertEquals("type", WAKELOCK_TYPE_1, info.get(0).type); + assertEquals("duration", TS_3 - TS_1, info.get(0).uptimeMillis); + assertEquals("count", 0, info.get(0).completedCount); + } } @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/NotificationAssistantsTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java index 19b90b6b76d9..076e3e9fcc24 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java @@ -22,6 +22,7 @@ import static android.service.notification.Adjustment.KEY_TYPE; import static android.service.notification.Adjustment.TYPE_CONTENT_RECOMMENDATION; import static android.service.notification.Adjustment.TYPE_NEWS; import static android.service.notification.Adjustment.TYPE_PROMOTION; +import static android.service.notification.Adjustment.TYPE_SOCIAL_MEDIA; import static com.android.server.notification.NotificationManagerService.DEFAULT_ALLOWED_ADJUSTMENTS; @@ -611,7 +612,8 @@ public class NotificationAssistantsTest extends UiServiceTestCase { ManagedServices.ManagedServiceInfo info = mAssistants.new ManagedServiceInfo(null, mCn, userId, false, null, 35, 2345256); - mAssistants.setAdjustmentTypeSupportedState(info, Adjustment.KEY_NOT_CONVERSATION, false); + mAssistants.setAdjustmentTypeSupportedState( + info.userid, Adjustment.KEY_NOT_CONVERSATION, false); assertThat(mAssistants.getUnsupportedAdjustments(userId)).contains( Adjustment.KEY_NOT_CONVERSATION); @@ -632,7 +634,8 @@ public class NotificationAssistantsTest extends UiServiceTestCase { ManagedServices.ManagedServiceInfo info = mAssistants.new ManagedServiceInfo(null, mCn, userId, false, null, 35, 2345256); - mAssistants.setAdjustmentTypeSupportedState(info, Adjustment.KEY_NOT_CONVERSATION, false); + mAssistants.setAdjustmentTypeSupportedState( + info.userid, Adjustment.KEY_NOT_CONVERSATION, false); writeXmlAndReload(USER_ALL); @@ -654,7 +657,6 @@ public class NotificationAssistantsTest extends UiServiceTestCase { assertNotNull(current); writeXmlAndReload(USER_ALL); - assertThat(mAssistants.getUnsupportedAdjustments(userId).size()).isEqualTo(0); } @@ -707,26 +709,29 @@ public class NotificationAssistantsTest extends UiServiceTestCase { @Test @EnableFlags(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION) public void testSetAssistantAdjustmentKeyTypeState_allow() { - assertThat(mAssistants.getAllowedClassificationTypes()).asList() - .containsExactly(TYPE_PROMOTION); + mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_CONTENT_RECOMMENDATION, false); + assertThat(mAssistants.getAllowedClassificationTypes()) + .asList().doesNotContain(TYPE_CONTENT_RECOMMENDATION); mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_CONTENT_RECOMMENDATION, true); assertThat(mAssistants.getAllowedClassificationTypes()).asList() - .containsExactlyElementsIn(List.of(TYPE_PROMOTION, TYPE_CONTENT_RECOMMENDATION)); + .contains(TYPE_CONTENT_RECOMMENDATION); } @Test @EnableFlags(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION) public void testSetAssistantAdjustmentKeyTypeState_disallow() { mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_PROMOTION, false); - assertThat(mAssistants.getAllowedClassificationTypes()).isEmpty(); + assertThat(mAssistants.getAllowedClassificationTypes()) + .asList().doesNotContain(TYPE_PROMOTION); } @Test @EnableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI) public void testDisallowAdjustmentKeyType_readWriteXml() throws Exception { mAssistants.loadDefaultsFromConfig(true); + mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_SOCIAL_MEDIA, false); mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_PROMOTION, false); mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_NEWS, true); mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_CONTENT_RECOMMENDATION, true); @@ -745,7 +750,8 @@ public class NotificationAssistantsTest extends UiServiceTestCase { writeXmlAndReload(USER_ALL); assertThat(mAssistants.getAllowedClassificationTypes()).asList() - .containsExactly(TYPE_PROMOTION); + .containsExactlyElementsIn(List.of(TYPE_PROMOTION, TYPE_NEWS, TYPE_SOCIAL_MEDIA, + TYPE_CONTENT_RECOMMENDATION)); } @Test @@ -757,18 +763,22 @@ public class NotificationAssistantsTest extends UiServiceTestCase { String allowedPackage = "allowed.package"; assertThat(mAssistants.isAdjustmentAllowedForPackage(key, allowedPackage)).isTrue(); + assertThat(mAssistants.getAdjustmentDeniedPackages(key)).isEmpty(); // Set type adjustment disallowed for this package mAssistants.setAdjustmentSupportedForPackage(key, allowedPackage, false); // Then the package is marked as denied assertThat(mAssistants.isAdjustmentAllowedForPackage(key, allowedPackage)).isFalse(); + assertThat(mAssistants.getAdjustmentDeniedPackages(key)).asList() + .containsExactly(allowedPackage); // Set type adjustment allowed again mAssistants.setAdjustmentSupportedForPackage(key, allowedPackage, true); // Then the package is marked as allowed again assertThat(mAssistants.isAdjustmentAllowedForPackage(key, allowedPackage)).isTrue(); + assertThat(mAssistants.getAdjustmentDeniedPackages(key)).isEmpty(); } @Test @@ -789,6 +799,8 @@ public class NotificationAssistantsTest extends UiServiceTestCase { assertThat(mAssistants.isAdjustmentAllowedForPackage(key, deniedPkg1)).isFalse(); assertThat(mAssistants.isAdjustmentAllowedForPackage(key, deniedPkg2)).isFalse(); assertThat(mAssistants.isAdjustmentAllowedForPackage(key, deniedPkg3)).isFalse(); + assertThat(mAssistants.getAdjustmentDeniedPackages(key)).asList() + .containsExactlyElementsIn(List.of(deniedPkg1, deniedPkg2, deniedPkg3)); // And when we re-allow one of them, mAssistants.setAdjustmentSupportedForPackage(key, deniedPkg2, true); @@ -797,6 +809,8 @@ public class NotificationAssistantsTest extends UiServiceTestCase { assertThat(mAssistants.isAdjustmentAllowedForPackage(key, deniedPkg1)).isFalse(); assertThat(mAssistants.isAdjustmentAllowedForPackage(key, deniedPkg2)).isTrue(); assertThat(mAssistants.isAdjustmentAllowedForPackage(key, deniedPkg3)).isFalse(); + assertThat(mAssistants.getAdjustmentDeniedPackages(key)).asList() + .containsExactlyElementsIn(List.of(deniedPkg1, deniedPkg3)); } @Test @@ -860,8 +874,9 @@ public class NotificationAssistantsTest extends UiServiceTestCase { mAssistants.new ManagedServiceInfo(null, mCn, userId, false, null, 35, 2345256); // Ensure bundling is enabled - mAssistants.setAdjustmentTypeSupportedState(info, KEY_TYPE, true); + mAssistants.setAdjustmentTypeSupportedState(info.userid, KEY_TYPE, true); // Enable these specific bundle types + mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_SOCIAL_MEDIA, false); mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_PROMOTION, false); mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_NEWS, true); mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_CONTENT_RECOMMENDATION, true); @@ -894,7 +909,7 @@ public class NotificationAssistantsTest extends UiServiceTestCase { .isEqualTo(NotificationProtoEnums.TYPE_CONTENT_RECOMMENDATION); // Disable the top-level bundling setting - mAssistants.setAdjustmentTypeSupportedState(info, KEY_TYPE, false); + mAssistants.setAdjustmentTypeSupportedState(info.userid, KEY_TYPE, false); // Enable these specific bundle types mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_PROMOTION, true); mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_NEWS, false); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index a9e8f4a0d965..cc68e4e73a4f 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -18,7 +18,6 @@ package com.android.server.notification; import static android.app.AppOpsManager.MODE_ALLOWED; import static android.app.AppOpsManager.MODE_DEFAULT; import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW; -import static android.app.Flags.FLAG_MODES_UI; import static android.app.Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI; import static android.app.Notification.VISIBILITY_PRIVATE; import static android.app.Notification.VISIBILITY_SECRET; @@ -259,10 +258,9 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Parameters(name = "{0}") public static List<FlagsParameterization> getParams() { return FlagsParameterization.allCombinationsOf( - android.app.Flags.FLAG_API_RICH_ONGOING, android.app.Flags.FLAG_UI_RICH_ONGOING, - FLAG_NOTIFICATION_CLASSIFICATION, FLAG_NOTIFICATION_CLASSIFICATION_UI, - FLAG_MODES_UI, android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS); + FLAG_NOTIFICATION_CLASSIFICATION_UI, + android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS); } public PreferencesHelperTest(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/CameraCompatFreeformPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java index 716f86418bcb..560725241853 100644 --- a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java @@ -158,7 +158,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { public void testIsCameraRunningAndWindowingModeEligible_notFreeformWindowing_returnsFalse() { configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); assertFalse(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity)); } @@ -169,7 +169,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { public void testIsCameraRunningAndWindowingModeEligible_optInFreeformCameraRunning_true() { configureActivity(SCREEN_ORIENTATION_PORTRAIT); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); assertTrue(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity)); } @@ -179,7 +179,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { public void testIsFreeformLetterboxingForCameraAllowed_overrideDisabled_returnsFalse() { configureActivity(SCREEN_ORIENTATION_PORTRAIT); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); assertFalse(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity)); } @@ -199,7 +199,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { public void testIsFreeformLetterboxingForCameraAllowed_notFreeformWindowing_returnsFalse() { configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); assertFalse(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity)); } @@ -210,7 +210,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { public void testIsFreeformLetterboxingForCameraAllowed_optInFreeformCameraRunning_true() { configureActivity(SCREEN_ORIENTATION_PORTRAIT); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); assertTrue(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity)); } @@ -222,7 +222,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN); doReturn(false).when(mActivity).inFreeformWindowingMode(); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); assertNotInCameraCompatMode(); } @@ -250,7 +250,8 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { public void testCameraConnected_deviceInPortrait_portraitCameraCompatMode() throws Exception { configureActivity(SCREEN_ORIENTATION_PORTRAIT); setDisplayRotation(ROTATION_0); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_PORTRAIT); assertActivityRefreshRequested(/* refreshRequested */ false); @@ -262,7 +263,8 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { public void testCameraConnected_deviceInLandscape_portraitCameraCompatMode() throws Exception { configureActivity(SCREEN_ORIENTATION_PORTRAIT); setDisplayRotation(ROTATION_270); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE); assertActivityRefreshRequested(/* refreshRequested */ false); @@ -274,7 +276,8 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { public void testCameraConnected_deviceInPortrait_landscapeCameraCompatMode() throws Exception { configureActivity(SCREEN_ORIENTATION_LANDSCAPE); setDisplayRotation(ROTATION_0); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_PORTRAIT); assertActivityRefreshRequested(/* refreshRequested */ false); @@ -286,7 +289,8 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { public void testCameraConnected_deviceInLandscape_landscapeCameraCompatMode() throws Exception { configureActivity(SCREEN_ORIENTATION_LANDSCAPE); setDisplayRotation(ROTATION_270); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_LANDSCAPE); assertActivityRefreshRequested(/* refreshRequested */ false); @@ -299,12 +303,12 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { configureActivity(SCREEN_ORIENTATION_PORTRAIT); setDisplayRotation(ROTATION_270); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); callOnActivityConfigurationChanging(mActivity, /* letterboxNew= */ true, /* lastLetterbox= */ false); assertActivityRefreshRequested(/* refreshRequested */ true); - mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + onCameraClosed(CAMERA_ID_1); + onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); // Activity is letterboxed from the previous configuration change. callOnActivityConfigurationChanging(mActivity, /* letterboxNew= */ true, /* lastLetterbox= */ true); @@ -319,7 +323,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { public void testCameraOpenedForDifferentPackage_notInCameraCompatMode() { configureActivity(SCREEN_ORIENTATION_PORTRAIT); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_2); + onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_2); assertNotInCameraCompatMode(); } @@ -329,7 +333,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { public void testShouldApplyCameraCompatFreeformTreatment_overrideNotEnabled_returnsFalse() { configureActivity(SCREEN_ORIENTATION_PORTRAIT); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); assertFalse(mCameraCompatFreeformPolicy.isTreatmentEnabledForActivity(mActivity, /* checkOrientation */ true)); @@ -341,7 +345,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { public void testShouldApplyCameraCompatFreeformTreatment_enabledByOverride_returnsTrue() { configureActivity(SCREEN_ORIENTATION_PORTRAIT); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); assertTrue(mActivity.info .isChangeEnabled(OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT)); @@ -356,7 +360,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { configureActivity(SCREEN_ORIENTATION_PORTRAIT); Configuration oldConfiguration = createConfiguration(/* letterbox= */ false); Configuration newConfiguration = createConfiguration(/* letterbox= */ true); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); assertTrue(mCameraCompatFreeformPolicy.shouldRefreshActivity(mActivity, newConfiguration, oldConfiguration)); @@ -372,7 +376,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { oldConfiguration.windowConfiguration.setDisplayRotation(0); newConfiguration.windowConfiguration.setDisplayRotation(90); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); assertTrue(mCameraCompatFreeformPolicy.shouldRefreshActivity(mActivity, newConfiguration, oldConfiguration)); @@ -388,7 +392,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { oldConfiguration.windowConfiguration.setDisplayRotation(0); newConfiguration.windowConfiguration.setDisplayRotation(0); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); assertFalse(mCameraCompatFreeformPolicy.shouldRefreshActivity(mActivity, newConfiguration, oldConfiguration)); @@ -404,7 +408,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { doReturn(false).when(mActivity.mAppCompatController.getCameraOverrides()) .shouldRefreshActivityForCameraCompat(); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); callOnActivityConfigurationChanging(mActivity); assertActivityRefreshRequested(/* refreshRequested */ false); @@ -419,7 +423,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { configureActivity(SCREEN_ORIENTATION_PORTRAIT); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); callOnActivityConfigurationChanging(mActivity); assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false); @@ -434,7 +438,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { doReturn(true).when(mActivity.mAppCompatController.getCameraOverrides()) .shouldRefreshActivityViaPauseForCameraCompat(); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); callOnActivityConfigurationChanging(mActivity); assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false); @@ -446,7 +450,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { public void testGetCameraCompatAspectRatio_activityNotInCameraCompat_returnsDefaultAspRatio() { configureActivity(SCREEN_ORIENTATION_FULL_USER); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); callOnActivityConfigurationChanging(mActivity); assertEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO, @@ -462,7 +466,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { final float configAspectRatio = 1.5f; mWm.mAppCompatConfiguration.setCameraCompatAspectRatio(configAspectRatio); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); callOnActivityConfigurationChanging(mActivity); assertEquals(configAspectRatio, @@ -480,7 +484,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { doReturn(true).when(mActivity.mAppCompatController.getCameraOverrides()) .isOverrideMinAspectRatioForCameraEnabled(); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); callOnActivityConfigurationChanging(mActivity); assertEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO, @@ -496,7 +500,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { configureActivity(SCREEN_ORIENTATION_PORTRAIT); setDisplayRotation(ROTATION_270); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); // This is a portrait rotation for a device with portrait natural orientation (most common, // currently the only one supported). @@ -511,7 +515,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { configureActivity(SCREEN_ORIENTATION_LANDSCAPE); setDisplayRotation(ROTATION_0); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); // This is a landscape rotation for a device with portrait natural orientation (most common, // currently the only one supported). @@ -616,6 +620,16 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { .inFreeformWindowingMode(); } + private void onCameraOpened(@NonNull String cameraId, @NonNull String packageName) { + mCameraAvailabilityCallback.onCameraOpened(cameraId, packageName); + waitHandlerIdle(mDisplayContent.mWmService.mH); + } + + private void onCameraClosed(@NonNull String cameraId) { + mCameraAvailabilityCallback.onCameraClosed(cameraId); + waitHandlerIdle(mDisplayContent.mWmService.mH); + } + private void assertInCameraCompatMode(@CameraCompatTaskInfo.FreeformCameraCompatMode int mode) { assertEquals(mode, mCameraCompatFreeformPolicy.getCameraCompatMode(mActivity)); } diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java index cdb51fc1c645..fdde3b38f19f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java @@ -1345,7 +1345,7 @@ public class DesktopModeLaunchParamsModifierTests extends private void setupDesktopModeLaunchParamsModifier(boolean isDesktopModeSupported, boolean enforceDeviceRestrictions) { doReturn(isDesktopModeSupported) - .when(() -> DesktopModeHelper.isDesktopModeSupported(any())); + .when(() -> DesktopModeHelper.isDeviceEligibleForDesktopMode(any())); doReturn(enforceDeviceRestrictions) .when(DesktopModeHelper::shouldEnforceDeviceRestrictions); } 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); } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java index 7e7a53148603..cd1c48554be9 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java @@ -2747,8 +2747,7 @@ public class VoiceInteractionManagerService extends SystemService { isManagedProfileVisible = true; } } - final ScreenCapture.ScreenshotHardwareBuffer shb = - mWmInternal.takeAssistScreenshot(/* windowTypesToExclude= */ Set.of()); + final ScreenCapture.ScreenshotHardwareBuffer shb = mWmInternal.takeAssistScreenshot(); final Bitmap bm = shb != null ? shb.asBitmap() : null; // Now that everything is fetched, putting it in the launchIntent. if (bm != null) { diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt index 9e9d014c622d..55d6fd9b4a73 100644 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt @@ -87,14 +87,18 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : wmHelper: WindowManagerStateHelper, device: UiDevice, motionEventHelper: MotionEventHelper = MotionEventHelper(getInstrumentation(), TOUCH), + shouldUseDragToDesktop: Boolean = false, ) { innerHelper.launchViaIntent(wmHelper) - if (!isInDesktopWindowingMode(wmHelper)) { + if (isInDesktopWindowingMode(wmHelper)) return + if (shouldUseDragToDesktop) { enterDesktopModeWithDrag( wmHelper = wmHelper, device = device, motionEventHelper = motionEventHelper ) + } else { + enterDesktopModeFromAppHandleMenu(wmHelper, device) } } diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java index 61fa7b542bc0..83d22d923c78 100644 --- a/tests/utils/testutils/java/android/os/test/TestLooper.java +++ b/tests/utils/testutils/java/android/os/test/TestLooper.java @@ -18,24 +18,18 @@ package android.os.test; import static org.junit.Assert.assertTrue; -import android.os.Build; import android.os.Handler; import android.os.HandlerExecutor; import android.os.Looper; import android.os.Message; import android.os.MessageQueue; import android.os.SystemClock; -import android.os.TestLooperManager; import android.util.Log; -import androidx.test.InstrumentationRegistry; - import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.ArrayDeque; -import java.util.Queue; import java.util.concurrent.Executor; /** @@ -50,9 +44,7 @@ import java.util.concurrent.Executor; * The Robolectric class also allows advancing time. */ public class TestLooper { - private final Looper mLooper; - private final TestLooperManager mTestLooperManager; - private final Clock mClock; + protected final Looper mLooper; private static final Constructor<Looper> LOOPER_CONSTRUCTOR; private static final Field THREAD_LOCAL_LOOPER_FIELD; @@ -62,14 +54,9 @@ public class TestLooper { private static final Method MESSAGE_MARK_IN_USE_METHOD; private static final String TAG = "TestLooper"; - private AutoDispatchThread mAutoDispatchThread; + private final Clock mClock; - /** - * Baklava introduces new {@link TestLooperManager} APIs that we can use instead of reflection. - */ - private static boolean isAtLeastBaklava() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA; - } + private AutoDispatchThread mAutoDispatchThread; static { try { @@ -77,22 +64,14 @@ public class TestLooper { LOOPER_CONSTRUCTOR.setAccessible(true); THREAD_LOCAL_LOOPER_FIELD = Looper.class.getDeclaredField("sThreadLocal"); THREAD_LOCAL_LOOPER_FIELD.setAccessible(true); - - if (isAtLeastBaklava()) { - MESSAGE_QUEUE_MESSAGES_FIELD = null; - MESSAGE_NEXT_FIELD = null; - MESSAGE_WHEN_FIELD = null; - MESSAGE_MARK_IN_USE_METHOD = null; - } else { - MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages"); - MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true); - MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next"); - MESSAGE_NEXT_FIELD.setAccessible(true); - MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when"); - MESSAGE_WHEN_FIELD.setAccessible(true); - MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse"); - MESSAGE_MARK_IN_USE_METHOD.setAccessible(true); - } + MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages"); + MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true); + MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next"); + MESSAGE_NEXT_FIELD.setAccessible(true); + MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when"); + MESSAGE_WHEN_FIELD.setAccessible(true); + MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse"); + MESSAGE_MARK_IN_USE_METHOD.setAccessible(true); } catch (NoSuchFieldException | NoSuchMethodException e) { throw new RuntimeException("Failed to initialize TestLooper", e); } @@ -127,13 +106,6 @@ public class TestLooper { throw new RuntimeException("Reflection error constructing or accessing looper", e); } - if (isAtLeastBaklava()) { - mTestLooperManager = - InstrumentationRegistry.getInstrumentation().acquireLooperManager(mLooper); - } else { - mTestLooperManager = null; - } - mClock = clock; } @@ -145,72 +117,19 @@ public class TestLooper { return new HandlerExecutor(new Handler(getLooper())); } - private Message getMessageLinkedListLegacy() { + private Message getMessageLinkedList() { try { MessageQueue queue = mLooper.getQueue(); return (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(queue); } catch (IllegalAccessException e) { throw new RuntimeException("Access failed in TestLooper: get - MessageQueue.mMessages", - e); + e); } } public void moveTimeForward(long milliSeconds) { - if (isAtLeastBaklava()) { - moveTimeForwardBaklava(milliSeconds); - } else { - moveTimeForwardLegacy(milliSeconds); - } - } - - private void moveTimeForwardBaklava(long milliSeconds) { - // Drain all Messages from the queue. - Queue<Message> messages = new ArrayDeque<>(); - while (true) { - Message message = mTestLooperManager.poll(); - if (message == null) { - break; - } - - // Adjust the Message's delivery time. - long newWhen = message.when - milliSeconds; - if (newWhen < 0) { - newWhen = 0; - } - message.when = newWhen; - messages.add(message); - } - - // Repost all Messages back to the queuewith a new time. - while (true) { - Message message = messages.poll(); - if (message == null) { - break; - } - - Runnable callback = message.getCallback(); - Handler handler = message.getTarget(); - long when = message.getWhen(); - - // The Message cannot be re-enqueued because it is marked in use. - // Make a copy of the Message and recycle the original. - // This resets {@link Message#isInUse()} but retains all other content. - { - Message newMessage = Message.obtain(); - newMessage.copyFrom(message); - newMessage.setCallback(callback); - mTestLooperManager.recycle(message); - message = newMessage; - } - - // Send the Message back to its Handler to be re-enqueued. - handler.sendMessageAtTime(message, when); - } - } - - private void moveTimeForwardLegacy(long milliSeconds) { try { - Message msg = getMessageLinkedListLegacy(); + Message msg = getMessageLinkedList(); while (msg != null) { long updatedWhen = msg.getWhen() - milliSeconds; if (updatedWhen < 0) { @@ -228,12 +147,12 @@ public class TestLooper { return mClock.uptimeMillis(); } - private Message messageQueueNextLegacy() { + private Message messageQueueNext() { try { long now = currentTime(); Message prevMsg = null; - Message msg = getMessageLinkedListLegacy(); + Message msg = getMessageLinkedList(); if (msg != null && msg.getTarget() == null) { // Stalled by a barrier. Find the next asynchronous message in // the queue. @@ -266,46 +185,18 @@ public class TestLooper { /** * @return true if there are pending messages in the message queue */ - public boolean isIdle() { - if (isAtLeastBaklava()) { - return isIdleBaklava(); - } else { - return isIdleLegacy(); - } - } + public synchronized boolean isIdle() { + Message messageList = getMessageLinkedList(); - private boolean isIdleBaklava() { - Long when = mTestLooperManager.peekWhen(); - return when != null && currentTime() >= when; - } - - private synchronized boolean isIdleLegacy() { - Message messageList = getMessageLinkedListLegacy(); return messageList != null && currentTime() >= messageList.getWhen(); } /** * @return the next message in the Looper's message queue or null if there is none */ - public Message nextMessage() { - if (isAtLeastBaklava()) { - return nextMessageBaklava(); - } else { - return nextMessageLegacy(); - } - } - - private Message nextMessageBaklava() { + public synchronized Message nextMessage() { if (isIdle()) { - return mTestLooperManager.poll(); - } else { - return null; - } - } - - private synchronized Message nextMessageLegacy() { - if (isIdle()) { - return messageQueueNextLegacy(); + return messageQueueNext(); } else { return null; } @@ -315,26 +206,9 @@ public class TestLooper { * Dispatch the next message in the queue * Asserts that there is a message in the queue */ - public void dispatchNext() { - if (isAtLeastBaklava()) { - dispatchNextBaklava(); - } else { - dispatchNextLegacy(); - } - } - - private void dispatchNextBaklava() { - assertTrue(isIdle()); - Message msg = mTestLooperManager.poll(); - if (msg == null) { - return; - } - msg.getTarget().dispatchMessage(msg); - } - - private synchronized void dispatchNextLegacy() { + public synchronized void dispatchNext() { assertTrue(isIdle()); - Message msg = messageQueueNextLegacy(); + Message msg = messageQueueNext(); if (msg == null) { return; } diff --git a/tools/localedata/extract_icu_data.py b/tools/localedata/extract_icu_data.py index ec531275af1c..899cd7f9ce5e 100755 --- a/tools/localedata/extract_icu_data.py +++ b/tools/localedata/extract_icu_data.py @@ -180,7 +180,14 @@ def pack_script_to_uint32(script): def dump_representative_locales(representative_locales): """Dump the set of representative locales.""" - print() + print(''' +/* + * TODO: Consider turning the below switch statement into binary search + * to save the disk space when the table is larger in the future. + * Disassembled code shows that the jump table emitted by clang can be + * 4x larger than the data in disk size, but it depends on the optimization option. + * However, a switch statement will benefit from the future of compiler improvement. + */''') print('bool isLocaleRepresentative(uint32_t language_and_region, const char* script) {') print(' const uint64_t packed_locale =') print(' ((static_cast<uint64_t>(language_and_region)) << 32u) |') |