diff options
191 files changed, 4339 insertions, 2014 deletions
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/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/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/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/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/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/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/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 89eff5ea618c..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 @@ -1412,6 +1412,7 @@ public abstract class WMShellModule { IconProvider iconProvider, GlobalDragListener globalDragListener, Transitions transitions, + Lazy<BubbleController> bubbleControllerLazy, @ShellMainThread ShellExecutor mainExecutor) { return new DragAndDropController( context, @@ -1424,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/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 30e439755c1c..10ffd79717e3 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 @@ -1088,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()) { 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/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/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/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 438bf70c879d..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 @@ -291,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)) 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/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 8ebe414111b5..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) 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 27a043616802..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 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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; } |