diff options
557 files changed, 14163 insertions, 6812 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 071cc6555be7..e5c059ecbfb7 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -84,7 +84,7 @@ aconfig_declarations_group { "android.view.inputmethod.flags-aconfig-java", "android.webkit.flags-aconfig-java", "android.widget.flags-aconfig-java", - "android.xr.flags-aconfig-java", + "android.xr.flags-aconfig-java-export", "art_exported_aconfig_flags_lib", "backstage_power_flags_lib", "backup_flags_lib", @@ -989,15 +989,22 @@ java_aconfig_library { // XR aconfig_declarations { name: "android.xr.flags-aconfig", - package: "android.xr", container: "system", + exportable: true, + package: "android.xr", srcs: ["core/java/android/content/pm/xr.aconfig"], } java_aconfig_library { - name: "android.xr.flags-aconfig-java", + name: "android.xr.flags-aconfig-java-export", aconfig_declarations: "android.xr.flags-aconfig", defaults: ["framework-minus-apex-aconfig-java-defaults"], + min_sdk_version: "30", + mode: "exported", + apex_available: [ + "//apex_available:platform", + "com.android.permission", + ], } // android.app diff --git a/Android.bp b/Android.bp index 9d3b64d7335b..303fa2cd18da 100644 --- a/Android.bp +++ b/Android.bp @@ -583,6 +583,7 @@ java_library { "documents-ui-compat-config", "calendar-provider-compat-config", "contacts-provider-platform-compat-config", + "SystemUI-core-compat-config", ] + select(soong_config_variable("ANDROID", "release_crashrecovery_module"), { "true": [], default: [ diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java index 9871d713178e..ab8131ba5126 100644 --- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java +++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java @@ -264,6 +264,8 @@ public class AppStandbyController @GuardedBy("mCarrierPrivilegedLock") private boolean mHaveCarrierPrivilegedApps; + private final boolean mHasFeatureTelephonySubscription; + /** List of carrier-privileged apps that should be excluded from standby */ @GuardedBy("mCarrierPrivilegedLock") private List<String> mCarrierPrivilegedApps; @@ -603,6 +605,8 @@ public class AppStandbyController mContext = mInjector.getContext(); mHandler = new AppStandbyHandler(mInjector.getLooper()); mPackageManager = mContext.getPackageManager(); + mHasFeatureTelephonySubscription = mPackageManager.hasSystemFeature( + PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION); DeviceStateReceiver deviceStateReceiver = new DeviceStateReceiver(); IntentFilter deviceStates = new IntentFilter(BatteryManager.ACTION_CHARGING); @@ -1515,7 +1519,7 @@ public class AppStandbyController } // Check this last, as it can be the most expensive check - if (isCarrierApp(packageName)) { + if (mHasFeatureTelephonySubscription && isCarrierApp(packageName)) { return STANDBY_BUCKET_EXEMPTED; } diff --git a/api/OWNERS b/api/OWNERS index 965093c9ab38..f2bcf13d2d2e 100644 --- a/api/OWNERS +++ b/api/OWNERS @@ -9,4 +9,4 @@ per-file *.go,go.mod,go.work,go.work.sum = file:platform/build/soong:/OWNERS per-file Android.bp = file:platform/build/soong:/OWNERS #{LAST_RESORT_SUGGESTION} # For metalava team to disable lint checks in platform -per-file Android.bp = aurimas@google.com,emberrose@google.com +per-file Android.bp = aurimas@google.com diff --git a/cmds/am/am.sh b/cmds/am/am.sh index 76ec214cb446..f099be3e26a2 100755 --- a/cmds/am/am.sh +++ b/cmds/am/am.sh @@ -1,11 +1,10 @@ #!/system/bin/sh -# set to top-app process group -settaskprofile $$ SCHED_SP_TOP_APP >/dev/null 2>&1 || true - if [ "$1" != "instrument" ] ; then cmd activity "$@" else + # set to top-app process group for instrument + settaskprofile $$ SCHED_SP_TOP_APP >/dev/null 2>&1 || true base=/system export CLASSPATH=$base/framework/am.jar exec app_process $base/bin com.android.commands.am.Am "$@" diff --git a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java index 6310d32515c5..696bc82a9ffc 100644 --- a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java +++ b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java @@ -18,6 +18,7 @@ package com.android.commands.bmgr; import android.annotation.IntDef; import android.annotation.UserIdInt; +import android.app.ActivityManager; import android.app.backup.BackupManager; import android.app.backup.BackupManagerMonitor; import android.app.backup.BackupProgress; @@ -73,6 +74,8 @@ public class Bmgr { "Error: Could not access the backup transport. Is the system running?"; private static final String PM_NOT_RUNNING_ERR = "Error: Could not access the Package Manager. Is the system running?"; + private static final String INVALID_USER_ID_ERR_TEMPLATE = + "Error: Invalid user id (%d).\n"; private String[] mArgs; private int mNextArg; @@ -104,6 +107,11 @@ public class Bmgr { mArgs = args; mNextArg = 0; int userId = parseUserId(); + if (userId < 0) { + System.err.printf(INVALID_USER_ID_ERR_TEMPLATE, userId); + return; + } + String op = nextArg(); Slog.v(TAG, "Running " + op + " for user:" + userId); @@ -955,12 +963,15 @@ public class Bmgr { private int parseUserId() { String arg = nextArg(); - if ("--user".equals(arg)) { - return UserHandle.parseUserArg(nextArg()); - } else { + if (!"--user".equals(arg)) { mNextArg--; return UserHandle.USER_SYSTEM; } + int userId = UserHandle.parseUserArg(nextArg()); + if (userId == UserHandle.USER_CURRENT) { + userId = ActivityManager.getCurrentUser(); + } + return userId; } private static void showUsage() { diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp index b43905b19239..844e52c3ecf2 100644 --- a/cmds/bootanimation/BootAnimation.cpp +++ b/cmds/bootanimation/BootAnimation.cpp @@ -441,7 +441,7 @@ public: numEvents = mBootAnimation->mDisplayEventReceiver->getEvents(buffer, kBufferSize); for (size_t i = 0; i < static_cast<size_t>(numEvents); i++) { const auto& event = buffer[i]; - if (event.header.type == DisplayEventReceiver::DISPLAY_EVENT_HOTPLUG) { + if (event.header.type == DisplayEventType::DISPLAY_EVENT_HOTPLUG) { SLOGV("Hotplug received"); if (!event.hotplug.connected) { diff --git a/cmds/uinput/tests/Android.bp b/cmds/uinput/tests/Android.bp index e728bd270a46..516de3325f77 100644 --- a/cmds/uinput/tests/Android.bp +++ b/cmds/uinput/tests/Android.bp @@ -18,3 +18,17 @@ android_test { "device-tests", ], } + +android_ravenwood_test { + name: "UinputTestsRavenwood", + srcs: [ + "src/**/*.java", + ], + static_libs: [ + "androidx.test.runner", + "frameworks-base-testutils", + "platform-test-annotations", + "truth", + "uinput", + ], +} diff --git a/core/api/current.txt b/core/api/current.txt index dd606774b770..050cad4e1a52 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -38774,7 +38774,7 @@ package android.provider { } public static final class Telephony.Carriers implements android.provider.BaseColumns { - field @FlaggedApi("com.android.internal.telephony.flags.apn_setting_field_support_flag") public static final String ALWAYS_ON = "always_on"; + field public static final String ALWAYS_ON = "always_on"; field public static final String APN = "apn"; field public static final String AUTH_TYPE = "authtype"; field @Deprecated public static final String BEARER = "bearer"; @@ -38788,8 +38788,8 @@ package android.provider { field public static final String MMSPORT = "mmsport"; field public static final String MMSPROXY = "mmsproxy"; field @Deprecated public static final String MNC = "mnc"; - field @FlaggedApi("com.android.internal.telephony.flags.apn_setting_field_support_flag") public static final String MTU_V4 = "mtu_v4"; - field @FlaggedApi("com.android.internal.telephony.flags.apn_setting_field_support_flag") public static final String MTU_V6 = "mtu_v6"; + field public static final String MTU_V4 = "mtu_v4"; + field public static final String MTU_V6 = "mtu_v6"; field @Deprecated public static final String MVNO_MATCH_DATA = "mvno_match_data"; field @Deprecated public static final String MVNO_TYPE = "mvno_type"; field public static final String NAME = "name"; @@ -38805,8 +38805,8 @@ package android.provider { field public static final String SUBSCRIPTION_ID = "sub_id"; field public static final String TYPE = "type"; field public static final String USER = "user"; - field @FlaggedApi("com.android.internal.telephony.flags.apn_setting_field_support_flag") public static final String USER_EDITABLE = "user_editable"; - field @FlaggedApi("com.android.internal.telephony.flags.apn_setting_field_support_flag") public static final String USER_VISIBLE = "user_visible"; + field public static final String USER_EDITABLE = "user_editable"; + field public static final String USER_VISIBLE = "user_visible"; } public static final class Telephony.Mms implements android.provider.Telephony.BaseMmsColumns { @@ -47776,7 +47776,7 @@ package android.telephony.data { method public int getProxyPort(); method public int getRoamingProtocol(); method public String getUser(); - method @FlaggedApi("com.android.internal.telephony.flags.apn_setting_field_support_flag") public boolean isAlwaysOn(); + method public boolean isAlwaysOn(); method public boolean isEnabled(); method public boolean isPersistent(); method public void writeToParcel(@NonNull android.os.Parcel, int); @@ -47818,7 +47818,7 @@ package android.telephony.data { public static class ApnSetting.Builder { ctor public ApnSetting.Builder(); method public android.telephony.data.ApnSetting build(); - method @FlaggedApi("com.android.internal.telephony.flags.apn_setting_field_support_flag") @NonNull public android.telephony.data.ApnSetting.Builder setAlwaysOn(boolean); + method @NonNull public android.telephony.data.ApnSetting.Builder setAlwaysOn(boolean); method @NonNull public android.telephony.data.ApnSetting.Builder setApnName(@Nullable String); method @NonNull public android.telephony.data.ApnSetting.Builder setApnTypeBitmask(int); method @NonNull public android.telephony.data.ApnSetting.Builder setAuthType(int); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 9a848d423c9a..76cce7439454 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -3436,7 +3436,7 @@ package android.companion.virtual { method public void close(); method @NonNull public android.content.Context createContext(); method @NonNull public android.companion.virtual.audio.VirtualAudioDevice createVirtualAudioDevice(@NonNull android.hardware.display.VirtualDisplay, @Nullable java.util.concurrent.Executor, @Nullable android.companion.virtual.audio.VirtualAudioDevice.AudioConfigurationChangeCallback); - method @FlaggedApi("android.companion.virtual.flags.virtual_camera") @NonNull public android.companion.virtual.camera.VirtualCamera createVirtualCamera(@NonNull android.companion.virtual.camera.VirtualCameraConfig); + method @NonNull public android.companion.virtual.camera.VirtualCamera createVirtualCamera(@NonNull android.companion.virtual.camera.VirtualCameraConfig); method @Deprecated @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@IntRange(from=1) int, @IntRange(from=1) int, @IntRange(from=1) int, @Nullable android.view.Surface, int, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.display.VirtualDisplay.Callback); method @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull android.hardware.display.VirtualDisplayConfig, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.display.VirtualDisplay.Callback); method @NonNull public android.hardware.input.VirtualDpad createVirtualDpad(@NonNull android.hardware.input.VirtualDpadConfig); @@ -3499,7 +3499,7 @@ package android.companion.virtual { field public static final int POLICY_TYPE_ACTIVITY = 3; // 0x3 field public static final int POLICY_TYPE_AUDIO = 1; // 0x1 field @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") public static final int POLICY_TYPE_BLOCKED_ACTIVITY = 6; // 0x6 - field @FlaggedApi("android.companion.virtual.flags.virtual_camera") public static final int POLICY_TYPE_CAMERA = 5; // 0x5 + field public static final int POLICY_TYPE_CAMERA = 5; // 0x5 field public static final int POLICY_TYPE_CLIPBOARD = 4; // 0x4 field @FlaggedApi("android.companion.virtualdevice.flags.default_device_camera_access_policy") public static final int POLICY_TYPE_DEFAULT_DEVICE_CAMERA_ACCESS = 7; // 0x7 field public static final int POLICY_TYPE_RECENTS = 2; // 0x2 @@ -3577,18 +3577,18 @@ package android.companion.virtual.audio { package android.companion.virtual.camera { - @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCamera implements java.io.Closeable { + public final class VirtualCamera implements java.io.Closeable { method public void close(); method @NonNull public android.companion.virtual.camera.VirtualCameraConfig getConfig(); } - @FlaggedApi("android.companion.virtual.flags.virtual_camera") public interface VirtualCameraCallback { + public interface VirtualCameraCallback { method public default void onProcessCaptureRequest(int, long); method public void onStreamClosed(int); method public void onStreamConfigured(int, @NonNull android.view.Surface, @IntRange(from=1) int, @IntRange(from=1) int, int); } - @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCameraConfig implements android.os.Parcelable { + public final class VirtualCameraConfig implements android.os.Parcelable { method public int describeContents(); method public int getLensFacing(); method @NonNull public String getName(); @@ -3602,7 +3602,7 @@ package android.companion.virtual.camera { field public static final int SENSOR_ORIENTATION_90 = 90; // 0x5a } - @FlaggedApi("android.companion.virtual.flags.virtual_camera") public static final class VirtualCameraConfig.Builder { + public static final class VirtualCameraConfig.Builder { ctor public VirtualCameraConfig.Builder(@NonNull String); method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder addStreamConfig(@IntRange(from=1) int, @IntRange(from=1) int, int, @IntRange(from=1) int); method @NonNull public android.companion.virtual.camera.VirtualCameraConfig build(); @@ -3611,7 +3611,7 @@ package android.companion.virtual.camera { method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setVirtualCameraCallback(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.camera.VirtualCameraCallback); } - @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCameraStreamConfig implements android.os.Parcelable { + public final class VirtualCameraStreamConfig implements android.os.Parcelable { method public int describeContents(); method public int getFormat(); method @IntRange(from=1) public int getHeight(); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 5453e735ce17..e8ff546cc61a 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -942,7 +942,7 @@ package android.companion.virtual { package android.companion.virtual.camera { - @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCamera implements java.io.Closeable { + public final class VirtualCamera implements java.io.Closeable { method @NonNull public String getId(); } diff --git a/core/java/android/accessibilityservice/OWNERS b/core/java/android/accessibilityservice/OWNERS index 1265dfa2c441..dac64f47ba7e 100644 --- a/core/java/android/accessibilityservice/OWNERS +++ b/core/java/android/accessibilityservice/OWNERS @@ -1,4 +1,7 @@ -# Bug component: 44215 +# Bug component: 1530954 +# +# The above component is for automated test bugs. If you are a human looking to report +# a bug in this codebase then please use component 44215. # Android Accessibility Framework owners include /services/accessibility/OWNERS
\ No newline at end of file diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index 82c746a8ad4c..b8c20bd97264 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -2230,6 +2230,16 @@ public class ActivityOptions extends ComponentOptions { return mLaunchCookie; } + /** + * Set the ability for the current transition/animation to work cross-task. + * @param allowTaskOverride true to allow cross-task use, otherwise false. + * + * @hide + */ + public ActivityOptions setOverrideTaskTransition(boolean allowTaskOverride) { + this.mOverrideTaskTransition = allowTaskOverride; + return this; + } /** @hide */ public boolean getOverrideTaskTransition() { diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index 2dead565fa85..f2e7e8513116 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -17,7 +17,6 @@ package android.app; import static android.app.PropertyInvalidatedCache.MODULE_SYSTEM; -import static android.app.PropertyInvalidatedCache.createSystemCacheKey; import static android.app.admin.DevicePolicyResources.Drawables.Style.SOLID_COLORED; import static android.app.admin.DevicePolicyResources.Drawables.Style.SOLID_NOT_COLORED; import static android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_ICON; @@ -1146,12 +1145,16 @@ public class ApplicationPackageManager extends PackageManager { } } - private static final String CACHE_KEY_PACKAGES_FOR_UID_PROPERTY = - createSystemCacheKey("get_packages_for_uid"); - private static final PropertyInvalidatedCache<Integer, GetPackagesForUidResult> - mGetPackagesForUidCache = - new PropertyInvalidatedCache<Integer, GetPackagesForUidResult>( - 1024, CACHE_KEY_PACKAGES_FOR_UID_PROPERTY) { + private static final String CACHE_KEY_PACKAGES_FOR_UID_API = "get_packages_for_uid"; + + /** @hide */ + @VisibleForTesting + public static final PropertyInvalidatedCache<Integer, GetPackagesForUidResult> + sGetPackagesForUidCache = new PropertyInvalidatedCache<>( + new PropertyInvalidatedCache.Args(MODULE_SYSTEM) + .maxEntries(1024).api(CACHE_KEY_PACKAGES_FOR_UID_API).cacheNulls(true), + CACHE_KEY_PACKAGES_FOR_UID_API, null) { + @Override public GetPackagesForUidResult recompute(Integer uid) { try { @@ -1170,17 +1173,17 @@ public class ApplicationPackageManager extends PackageManager { @Override public String[] getPackagesForUid(int uid) { - return mGetPackagesForUidCache.query(uid).value(); + return sGetPackagesForUidCache.query(uid).value(); } /** @hide */ public static void disableGetPackagesForUidCache() { - mGetPackagesForUidCache.disableLocal(); + sGetPackagesForUidCache.disableLocal(); } /** @hide */ public static void invalidateGetPackagesForUidCache() { - PropertyInvalidatedCache.invalidateCache(CACHE_KEY_PACKAGES_FOR_UID_PROPERTY); + sGetPackagesForUidCache.invalidateCache(); } @Override diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index dce15b833bbb..3633b4eb333a 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -11370,7 +11370,7 @@ public class Notification implements Parcelable if (mProgressPoints == null) { mProgressPoints = new ArrayList<>(); } - if (point.getPosition() >= 0) { + if (point.getPosition() > 0) { mProgressPoints.add(point); if (mProgressPoints.size() > MAX_PROGRESS_POINT_LIMIT) { @@ -11379,7 +11379,7 @@ public class Notification implements Parcelable } } else { - Log.w(TAG, "Dropped the point. The position is a negative integer."); + Log.w(TAG, "Dropped the point. The position is a negative or zero integer."); } return this; @@ -11893,7 +11893,9 @@ public class Notification implements Parcelable final List<Point> points = new ArrayList<>(); for (Point point : mProgressPoints) { final int position = point.getPosition(); - if (position < 0 || position > totalLength) continue; + // The points at start/end aren't supposed to show in the progress bar. + // Therefore those are also dropped here. + if (position <= 0 || position >= totalLength) continue; points.add(sanitizePoint(point, backgroundColor, defaultProgressColor)); if (points.size() == MAX_PROGRESS_POINT_LIMIT) { break; diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java index bfb33f2b7cb1..c573161f30cc 100644 --- a/core/java/android/app/PropertyInvalidatedCache.java +++ b/core/java/android/app/PropertyInvalidatedCache.java @@ -1163,6 +1163,17 @@ public class PropertyInvalidatedCache<Query, Result> { } /** + * Return the current cache nonce. + * @hide + */ + @VisibleForTesting + public long getNonce() { + synchronized (mLock) { + return mNonce.getNonce(); + } + } + + /** * Complete key prefixes. */ private static final String PREFIX_TEST = CACHE_KEY_PREFIX + "." + MODULE_TEST + "."; @@ -1314,7 +1325,7 @@ public class PropertyInvalidatedCache<Query, Result> { /** * Burst a property name into module and api. Throw if the key is invalid. This method is - * used in to transition legacy cache constructors to the args constructor. + * used to transition legacy cache constructors to the Args constructor. */ private static Args argsFromProperty(@NonNull String name) { throwIfInvalidCacheKey(name); @@ -1327,6 +1338,15 @@ public class PropertyInvalidatedCache<Query, Result> { } /** + * Return the API porting of a legacy property. This method is used to transition caches to + * the Args constructor. + * @hide + */ + public static String apiFromProperty(@NonNull String name) { + return argsFromProperty(name).mApi; + } + + /** * Make a new property invalidated cache. This constructor names the cache after the * property name. New clients should prefer the constructor that takes an explicit * cache name. @@ -2036,11 +2056,11 @@ public class PropertyInvalidatedCache<Query, Result> { } /** - * Disable all caches in the local process. This is primarily useful for testing when - * the test needs to bypass the cache or when the test is for a server, and the test - * process does not have privileges to write SystemProperties. Once disabled it is not - * possible to re-enable caching in the current process. If a client wants to - * temporarily disable caching, use the corking mechanism. + * Disable all caches in the local process. This is primarily useful for testing when the + * test needs to bypass the cache or when the test is for a server, and the test process does + * not have privileges to write the nonce. Once disabled it is not possible to re-enable + * caching in the current process. See {@link #testPropertyName} for a more focused way to + * bypass caches when the test is for a server. * @hide */ public static void disableForTestMode() { diff --git a/core/java/android/app/appfunctions/AppFunctionManager.java b/core/java/android/app/appfunctions/AppFunctionManager.java index 0a3891fe47a1..a66d59ba9cb6 100644 --- a/core/java/android/app/appfunctions/AppFunctionManager.java +++ b/core/java/android/app/appfunctions/AppFunctionManager.java @@ -27,6 +27,7 @@ import android.annotation.NonNull; import android.annotation.RequiresPermission; import android.annotation.SystemService; import android.annotation.UserHandleAware; +import android.app.appfunctions.AppFunctionManagerHelper.AppFunctionNotFoundException; import android.app.appsearch.AppSearchManager; import android.content.Context; import android.os.CancellationSignal; @@ -325,8 +326,28 @@ public final class AppFunctionManager { return; } + // Wrap the callback to convert AppFunctionNotFoundException to IllegalArgumentException + // to match the documentation. + OutcomeReceiver<Boolean, Exception> callbackWithExceptionInterceptor = + new OutcomeReceiver<>() { + @Override + public void onResult(@NonNull Boolean result) { + callback.onResult(result); + } + + @Override + public void onError(@NonNull Exception exception) { + if (exception instanceof AppFunctionNotFoundException) { + exception = new IllegalArgumentException(exception); + } + callback.onError(exception); + } + }; + AppFunctionManagerHelper.isAppFunctionEnabled( - functionIdentifier, targetPackage, appSearchManager, executor, callback); + functionIdentifier, targetPackage, appSearchManager, executor, + callbackWithExceptionInterceptor); + } private static class CallbackWrapper extends IAppFunctionEnabledCallback.Stub { diff --git a/core/java/android/app/appfunctions/AppFunctionManagerHelper.java b/core/java/android/app/appfunctions/AppFunctionManagerHelper.java index cc3ca03f423d..83abc048af8a 100644 --- a/core/java/android/app/appfunctions/AppFunctionManagerHelper.java +++ b/core/java/android/app/appfunctions/AppFunctionManagerHelper.java @@ -60,8 +60,8 @@ public class AppFunctionManagerHelper { * <p>If operation fails, the callback's {@link OutcomeReceiver#onError} is called with errors: * * <ul> - * <li>{@link IllegalArgumentException}, if the function is not found or the caller does not - * have access to it. + * <li>{@link AppFunctionNotFoundException}, if the function is not found or the caller does + * not have access to it. * </ul> * * @param functionIdentifier the identifier of the app function to check (unique within the @@ -216,7 +216,7 @@ public class AppFunctionManagerHelper { private static @NonNull Exception failedResultToException( @NonNull AppSearchResult appSearchResult) { return switch (appSearchResult.getResultCode()) { - case AppSearchResult.RESULT_INVALID_ARGUMENT -> new IllegalArgumentException( + case AppSearchResult.RESULT_INVALID_ARGUMENT -> new AppFunctionNotFoundException( appSearchResult.getErrorMessage()); case AppSearchResult.RESULT_IO_ERROR -> new IOException( appSearchResult.getErrorMessage()); @@ -225,4 +225,15 @@ public class AppFunctionManagerHelper { default -> new IllegalStateException(appSearchResult.getErrorMessage()); }; } + + /** + * Throws when the app function is not found. + * + * @hide + */ + public static class AppFunctionNotFoundException extends RuntimeException { + private AppFunctionNotFoundException(@NonNull String errorMessage) { + super(errorMessage); + } + } } diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java index 566e78a8de35..2b0e941cf602 100644 --- a/core/java/android/companion/CompanionDeviceManager.java +++ b/core/java/android/companion/CompanionDeviceManager.java @@ -277,6 +277,23 @@ public final class CompanionDeviceManager { */ public static final int MESSAGE_ONEWAY_TO_WEARABLE = 0x43847987; // +TOW + + /** @hide */ + @IntDef(flag = true, prefix = { "TRANSPORT_FLAG_" }, value = { + TRANSPORT_FLAG_EXTEND_PATCH_DIFF, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface TransportFlags {} + + /** + * A security flag that allows transports to be attached to devices that may be more vulnerable + * due to infrequent updates. Can only be used for associations with + * {@link AssociationRequest#DEVICE_PROFILE_WEARABLE_SENSING} device profile. + * + * @hide + */ + public static final int TRANSPORT_FLAG_EXTEND_PATCH_DIFF = 1; + /** * Callback for applications to receive updates about and the outcome of * {@link AssociationRequest} issued via {@code associate()} call. @@ -1452,7 +1469,52 @@ public final class CompanionDeviceManager { } try { - final Transport transport = new Transport(associationId, in, out); + final Transport transport = new Transport(associationId, in, out, 0); + mTransports.put(associationId, transport); + transport.start(); + } catch (IOException e) { + throw new RuntimeException("Failed to attach transport", e); + } + } + } + + /** + * Attach a bidirectional communication stream to be used as a transport channel for + * transporting system data between associated devices. Flags can be provided to further + * customize the behavior of the transport. + * + * @param associationId id of the associated device. + * @param in Already connected stream of data incoming from remote + * associated device. + * @param out Already connected stream of data outgoing to remote associated + * device. + * @param flags Flags to customize transport behavior. + * @throws DeviceNotAssociatedException Thrown if the associationId was not previously + * associated with this app. + * + * @see #buildPermissionTransferUserConsentIntent(int) + * @see #startSystemDataTransfer(int, Executor, OutcomeReceiver) + * @see #detachSystemDataTransport(int) + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES) + public void attachSystemDataTransport(int associationId, + @NonNull InputStream in, + @NonNull OutputStream out, + @TransportFlags int flags) throws DeviceNotAssociatedException { + if (mService == null) { + Log.w(TAG, "CompanionDeviceManager service is not available."); + return; + } + + synchronized (mTransports) { + if (mTransports.contains(associationId)) { + detachSystemDataTransport(associationId); + } + + try { + final Transport transport = new Transport(associationId, in, out, flags); mTransports.put(associationId, transport); transport.start(); } catch (IOException e) { @@ -1931,16 +1993,22 @@ public final class CompanionDeviceManager { private final int mAssociationId; private final InputStream mRemoteIn; private final OutputStream mRemoteOut; + private final int mFlags; private InputStream mLocalIn; private OutputStream mLocalOut; private volatile boolean mStopped; - public Transport(int associationId, InputStream remoteIn, OutputStream remoteOut) { + Transport(int associationId, InputStream remoteIn, OutputStream remoteOut) { + this(associationId, remoteIn, remoteOut, 0); + } + + Transport(int associationId, InputStream remoteIn, OutputStream remoteOut, int flags) { mAssociationId = associationId; mRemoteIn = remoteIn; mRemoteOut = remoteOut; + mFlags = flags; } public void start() throws IOException { @@ -1957,7 +2025,7 @@ public final class CompanionDeviceManager { try { mService.attachSystemDataTransport(mContext.getOpPackageName(), - mContext.getUserId(), mAssociationId, remoteFd); + mContext.getUserId(), mAssociationId, remoteFd, mFlags); } catch (RemoteException e) { throw new IOException("Failed to configure transport", e); } diff --git a/core/java/android/companion/ICompanionDeviceManager.aidl b/core/java/android/companion/ICompanionDeviceManager.aidl index a2b7dd9c3d0e..787e8b65a736 100644 --- a/core/java/android/companion/ICompanionDeviceManager.aidl +++ b/core/java/android/companion/ICompanionDeviceManager.aidl @@ -113,7 +113,7 @@ interface ICompanionDeviceManager { in ISystemDataTransferCallback callback); @EnforcePermission("DELIVER_COMPANION_MESSAGES") - void attachSystemDataTransport(String packageName, int userId, int associationId, in ParcelFileDescriptor fd); + void attachSystemDataTransport(String packageName, int userId, int associationId, in ParcelFileDescriptor fd, int flags); @EnforcePermission("DELIVER_COMPANION_MESSAGES") void detachSystemDataTransport(String packageName, int userId, int associationId); diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index 252db824c69f..ab52db4b7a30 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -37,8 +37,8 @@ import android.companion.virtual.audio.VirtualAudioDevice; import android.companion.virtual.audio.VirtualAudioDevice.AudioConfigurationChangeCallback; import android.companion.virtual.camera.VirtualCamera; import android.companion.virtual.camera.VirtualCameraConfig; -import android.companion.virtual.flags.Flags; import android.companion.virtual.sensor.VirtualSensor; +import android.companion.virtualdevice.flags.Flags; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -624,7 +624,7 @@ public final class VirtualDeviceManager { * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY */ - @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER) + @FlaggedApi(Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER) public void goToSleep() { mVirtualDeviceInternal.goToSleep(); } @@ -642,7 +642,7 @@ public final class VirtualDeviceManager { * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY */ - @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER) + @FlaggedApi(Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER) public void wakeUp() { mVirtualDeviceInternal.wakeUp(); } @@ -838,7 +838,7 @@ public final class VirtualDeviceManager { * @see #removeActivityPolicyExemption(ActivityPolicyExemption) * @see #setDevicePolicy */ - @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API) + @FlaggedApi(Flags.FLAG_ACTIVITY_CONTROL_API) public void addActivityPolicyExemption(@NonNull ActivityPolicyExemption exemption) { mVirtualDeviceInternal.addActivityPolicyExemption(Objects.requireNonNull(exemption)); } @@ -853,7 +853,7 @@ public final class VirtualDeviceManager { * @see #addActivityPolicyExemption(ActivityPolicyExemption) * @see #setDevicePolicy */ - @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API) + @FlaggedApi(Flags.FLAG_ACTIVITY_CONTROL_API) public void removeActivityPolicyExemption(@NonNull ActivityPolicyExemption exemption) { mVirtualDeviceInternal.removeActivityPolicyExemption(Objects.requireNonNull(exemption)); } @@ -875,7 +875,7 @@ public final class VirtualDeviceManager { * @see VirtualDeviceParams#POLICY_TYPE_RECENTS * @see VirtualDeviceParams#POLICY_TYPE_ACTIVITY */ - @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API) + @FlaggedApi(Flags.FLAG_ACTIVITY_CONTROL_API) public void setDevicePolicy( @VirtualDeviceParams.DynamicDisplayPolicyType int policyType, @VirtualDeviceParams.DevicePolicy int devicePolicy, @@ -1037,10 +1037,10 @@ public final class VirtualDeviceManager { * @see android.view.InputDevice#SOURCE_ROTARY_ENCODER */ @NonNull - @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_VIRTUAL_ROTARY) + @FlaggedApi(Flags.FLAG_VIRTUAL_ROTARY) public VirtualRotaryEncoder createVirtualRotaryEncoder( @NonNull VirtualRotaryEncoderConfig config) { - if (!android.companion.virtualdevice.flags.Flags.virtualRotary()) { + if (!Flags.virtualRotary()) { throw new UnsupportedOperationException("Virtual rotary support not enabled"); } return mVirtualDeviceInternal.createVirtualRotaryEncoder(config); @@ -1084,12 +1084,7 @@ public final class VirtualDeviceManager { * @see VirtualDeviceParams#POLICY_TYPE_CAMERA */ @NonNull - @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA) public VirtualCamera createVirtualCamera(@NonNull VirtualCameraConfig config) { - if (!Flags.virtualCamera()) { - throw new UnsupportedOperationException( - "Flag is not enabled: %s".formatted(Flags.FLAG_VIRTUAL_CAMERA)); - } return mVirtualDeviceInternal.createVirtualCamera(Objects.requireNonNull(config)); } @@ -1252,7 +1247,7 @@ public final class VirtualDeviceManager { * @see VirtualDeviceParams#POLICY_TYPE_ACTIVITY * @see VirtualDevice#addActivityPolicyExemption(ActivityPolicyExemption) */ - @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API) + @FlaggedApi(Flags.FLAG_ACTIVITY_CONTROL_API) default void onActivityLaunchBlocked(int displayId, @NonNull ComponentName componentName, @NonNull UserHandle user, @Nullable IntentSender intentSender) {} @@ -1268,7 +1263,7 @@ public final class VirtualDeviceManager { * @see Display#FLAG_SECURE * @see WindowManager.LayoutParams#FLAG_SECURE */ - @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API) + @FlaggedApi(Flags.FLAG_ACTIVITY_CONTROL_API) default void onSecureWindowShown(int displayId, @NonNull ComponentName componentName, @NonNull UserHandle user) {} @@ -1284,7 +1279,7 @@ public final class VirtualDeviceManager { * @see Display#FLAG_SECURE * @see WindowManager.LayoutParams#FLAG_SECURE */ - @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API) + @FlaggedApi(Flags.FLAG_ACTIVITY_CONTROL_API) default void onSecureWindowHidden(int displayId) {} } diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java index 95dee9b72a88..699494790f35 100644 --- a/core/java/android/companion/virtual/VirtualDeviceParams.java +++ b/core/java/android/companion/virtual/VirtualDeviceParams.java @@ -29,12 +29,12 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.annotation.SystemApi; -import android.companion.virtual.flags.Flags; import android.companion.virtual.sensor.IVirtualSensorCallback; import android.companion.virtual.sensor.VirtualSensor; import android.companion.virtual.sensor.VirtualSensorCallback; import android.companion.virtual.sensor.VirtualSensorConfig; import android.companion.virtual.sensor.VirtualSensorDirectChannelCallback; +import android.companion.virtualdevice.flags.Flags; import android.content.ComponentName; import android.content.Context; import android.hardware.display.VirtualDisplayConfig; @@ -279,7 +279,6 @@ public final class VirtualDeviceParams implements Parcelable { * * @see Context#getDeviceId */ - @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA) public static final int POLICY_TYPE_CAMERA = 5; /** @@ -296,7 +295,7 @@ public final class VirtualDeviceParams implements Parcelable { * </ul> */ // TODO(b/333443509): Link to POLICY_TYPE_ACTIVITY - @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API) + @FlaggedApi(Flags.FLAG_ACTIVITY_CONTROL_API) public static final int POLICY_TYPE_BLOCKED_ACTIVITY = 6; /** @@ -310,8 +309,7 @@ public final class VirtualDeviceParams implements Parcelable { * * @see Context#DEVICE_ID_DEFAULT */ - @FlaggedApi(android.companion.virtualdevice.flags.Flags - .FLAG_DEFAULT_DEVICE_CAMERA_ACCESS_POLICY) + @FlaggedApi(Flags.FLAG_DEFAULT_DEVICE_CAMERA_ACCESS_POLICY) public static final int POLICY_TYPE_DEFAULT_DEVICE_CAMERA_ACCESS = 7; private final int mLockState; @@ -407,7 +405,7 @@ public final class VirtualDeviceParams implements Parcelable { * * @see Builder#setDimDuration(Duration) */ - @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER) + @FlaggedApi(Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER) public @NonNull Duration getDimDuration() { return Duration.ofMillis(mDimDuration); } @@ -417,7 +415,7 @@ public final class VirtualDeviceParams implements Parcelable { * * @see Builder#setDimDuration(Duration) */ - @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER) + @FlaggedApi(Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER) public @NonNull Duration getScreenOffTimeout() { return Duration.ofMillis(mScreenOffTimeout); } @@ -876,7 +874,7 @@ public final class VirtualDeviceParams implements Parcelable { * @see android.hardware.display.DisplayManager#VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR * @see #setScreenOffTimeout */ - @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER) + @FlaggedApi(Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER) @NonNull public Builder setDimDuration(@NonNull Duration dimDuration) { if (Objects.requireNonNull(dimDuration).compareTo(Duration.ZERO) < 0) { @@ -901,7 +899,7 @@ public final class VirtualDeviceParams implements Parcelable { * @see #setDimDuration * @see VirtualDeviceManager.VirtualDevice#goToSleep() */ - @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER) + @FlaggedApi(Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER) @NonNull public Builder setScreenOffTimeout(@NonNull Duration screenOffTimeout) { if (Objects.requireNonNull(screenOffTimeout).compareTo(Duration.ZERO) < 0) { @@ -1311,15 +1309,11 @@ public final class VirtualDeviceParams implements Parcelable { mScreenOffTimeout = INFINITE_TIMEOUT; } - if (!Flags.virtualCamera()) { - mDevicePolicies.delete(POLICY_TYPE_CAMERA); - } - - if (!android.companion.virtualdevice.flags.Flags.defaultDeviceCameraAccessPolicy()) { + if (!Flags.defaultDeviceCameraAccessPolicy()) { mDevicePolicies.delete(POLICY_TYPE_DEFAULT_DEVICE_CAMERA_ACCESS); } - if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) { + if (!Flags.activityControlApi()) { mDevicePolicies.delete(POLICY_TYPE_BLOCKED_ACTIVITY); } diff --git a/core/java/android/companion/virtual/camera/VirtualCamera.java b/core/java/android/companion/virtual/camera/VirtualCamera.java index ece048d3a95b..b7bcc29a39cb 100644 --- a/core/java/android/companion/virtual/camera/VirtualCamera.java +++ b/core/java/android/companion/virtual/camera/VirtualCamera.java @@ -16,14 +16,12 @@ package android.companion.virtual.camera; -import android.annotation.FlaggedApi; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.TestApi; import android.companion.virtual.IVirtualDevice; import android.companion.virtual.VirtualDeviceManager; import android.companion.virtual.VirtualDeviceParams; -import android.companion.virtual.flags.Flags; import android.hardware.camera2.CameraDevice; import android.os.RemoteException; @@ -51,7 +49,6 @@ import java.util.concurrent.Executor; * @hide */ @SystemApi -@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA) public final class VirtualCamera implements Closeable { private final IVirtualDevice mVirtualDevice; diff --git a/core/java/android/companion/virtual/camera/VirtualCameraCallback.java b/core/java/android/companion/virtual/camera/VirtualCameraCallback.java index c894de428b10..d326be83c404 100644 --- a/core/java/android/companion/virtual/camera/VirtualCameraCallback.java +++ b/core/java/android/companion/virtual/camera/VirtualCameraCallback.java @@ -16,11 +16,9 @@ package android.companion.virtual.camera; -import android.annotation.FlaggedApi; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.SystemApi; -import android.companion.virtual.flags.Flags; import android.graphics.ImageFormat; import android.view.Surface; @@ -34,7 +32,6 @@ import java.util.concurrent.Executor; * @hide */ @SystemApi -@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA) public interface VirtualCameraCallback { /** diff --git a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java index 769b658c78ce..6c88ec99349e 100644 --- a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java +++ b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java @@ -18,14 +18,12 @@ package android.companion.virtual.camera; import static java.util.Objects.requireNonNull; -import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.companion.virtual.VirtualDevice; -import android.companion.virtual.flags.Flags; import android.graphics.ImageFormat; import android.graphics.PixelFormat; import android.hardware.camera2.CameraMetadata; @@ -47,7 +45,6 @@ import java.util.concurrent.Executor; * @hide */ @SystemApi -@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA) public final class VirtualCameraConfig implements Parcelable { private static final int LENS_FACING_UNKNOWN = -1; @@ -198,7 +195,6 @@ public final class VirtualCameraConfig implements Parcelable { * VirtualCameraCallback)} * <li>A lens facing must be set with {@link #setLensFacing(int)} */ - @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA) public static final class Builder { private final String mName; diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java index 6ab66b3d2309..be498806697c 100644 --- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java +++ b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java @@ -16,11 +16,9 @@ package android.companion.virtual.camera; -import android.annotation.FlaggedApi; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.SystemApi; -import android.companion.virtual.flags.Flags; import android.graphics.ImageFormat; import android.os.Parcel; import android.os.Parcelable; @@ -35,7 +33,6 @@ import java.util.Objects; * @hide */ @SystemApi -@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA) public final class VirtualCameraStreamConfig implements Parcelable { // TODO(b/310857519): Check if we should increase the fps upper limit in future. static final int MAX_FPS_UPPER_LIMIT = 60; diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig index 1cf42820f356..fcdb02ab5da2 100644 --- a/core/java/android/companion/virtual/flags/flags.aconfig +++ b/core/java/android/companion/virtual/flags/flags.aconfig @@ -11,13 +11,6 @@ package: "android.companion.virtualdevice.flags" container: "system" flag { - namespace: "virtual_devices" - name: "virtual_camera_service_discovery" - description: "Enable discovery of the Virtual Camera HAL without a VINTF entry" - bug: "305170199" -} - -flag { namespace: "virtual_devices" name: "virtual_display_insets" description: "APIs for specifying virtual display insets (via cutout)" @@ -34,13 +27,6 @@ flag { } flag { - namespace: "virtual_devices" - name: "camera_device_awareness" - description: "Enable device awareness in camera service" - bug: "305170199" -} - -flag { name: "virtual_rotary" is_exported: true namespace: "virtual_devices" @@ -49,14 +35,6 @@ flag { } flag { - namespace: "virtual_devices" - name: "device_aware_drm" - description: "Makes MediaDrm APIs device-aware" - bug: "303535376" - is_fixed_read_only: true -} - -flag { namespace: "virtual_devices" name: "enforce_remote_device_opt_out_on_all_virtual_displays" description: "Respect canDisplayOnRemoteDevices on all virtual displays" diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index 37f3f17ebe42..e6450606d450 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -1643,6 +1643,19 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { public static final long OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION = 327313645L; /** + * When the override is enabled, the activity receives configuration coupled with caption bar + * insets. Normally, caption bar insets are decoupled from configuration. + * + * <p>Override applies only if the activity targets SDK level 34 or earlier version. + * + * @hide + */ + @ChangeId + @Overridable + @Disabled + public static final long OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS = 388014743L; + + /** * Optional set of a certificates identifying apps that are allowed to embed this activity. From * the "knownActivityEmbeddingCerts" attribute. */ diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 0369b7d9bc28..6ae2df2cd7a2 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -16,6 +16,7 @@ package android.content.pm; +import static android.app.PropertyInvalidatedCache.MODULE_SYSTEM; import static android.content.pm.SigningInfo.AppSigningSchemeVersion; import static android.media.audio.Flags.FLAG_FEATURE_SPATIAL_AUDIO_HEADTRACKING_LOW_LATENCY; @@ -11659,11 +11660,22 @@ public abstract class PackageManager { } } - private static final PropertyInvalidatedCache<ApplicationInfoQuery, ApplicationInfo> - sApplicationInfoCache = - new PropertyInvalidatedCache<ApplicationInfoQuery, ApplicationInfo>( - 2048, PermissionManager.CACHE_KEY_PACKAGE_INFO_CACHE, - "getApplicationInfo") { + private static String packageInfoApi() { + return PropertyInvalidatedCache.apiFromProperty( + PermissionManager.CACHE_KEY_PACKAGE_INFO_CACHE); + } + + // The maximum number of entries to keep in the packageInfo and applicationInfo caches. + private final static int MAX_INFO_CACHE_ENTRIES = 2048; + + /** @hide */ + @VisibleForTesting + public static final PropertyInvalidatedCache<ApplicationInfoQuery, ApplicationInfo> + sApplicationInfoCache = new PropertyInvalidatedCache<>( + new PropertyInvalidatedCache.Args(MODULE_SYSTEM) + .maxEntries(MAX_INFO_CACHE_ENTRIES).api(packageInfoApi()).cacheNulls(true), + "getApplicationInfo", null) { + @Override public ApplicationInfo recompute(ApplicationInfoQuery query) { return getApplicationInfoAsUserUncached( @@ -11749,10 +11761,11 @@ public abstract class PackageManager { } private static final PropertyInvalidatedCache<PackageInfoQuery, PackageInfo> - sPackageInfoCache = - new PropertyInvalidatedCache<PackageInfoQuery, PackageInfo>( - 2048, PermissionManager.CACHE_KEY_PACKAGE_INFO_CACHE, - "getPackageInfo") { + sPackageInfoCache = new PropertyInvalidatedCache<>( + new PropertyInvalidatedCache.Args(MODULE_SYSTEM) + .maxEntries(MAX_INFO_CACHE_ENTRIES).api(packageInfoApi()).cacheNulls(true), + "getPackageInfo", null) { + @Override public PackageInfo recompute(PackageInfoQuery query) { return getPackageInfoAsUserUncached( diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java index ca3e3d2ad61b..6ec70045f1f4 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -358,8 +358,7 @@ public class Camera { CameraInfo cameraInfo); private static int getDevicePolicyFromContext(Context context) { - if (context.getDeviceId() == DEVICE_ID_DEFAULT - || !android.companion.virtual.flags.Flags.virtualCamera()) { + if (context.getDeviceId() == DEVICE_ID_DEFAULT) { return DEVICE_POLICY_DEFAULT; } diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index bfaff941939c..bcb7ebfb286f 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -67,6 +67,7 @@ import android.os.Handler; import android.os.HandlerExecutor; import android.os.HandlerThread; import android.os.IBinder; +import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; import android.os.ServiceSpecificException; @@ -591,8 +592,7 @@ public final class CameraManager { /** @hide */ public int getDevicePolicyFromContext(@NonNull Context context) { - if (context.getDeviceId() == DEVICE_ID_DEFAULT - || !android.companion.virtual.flags.Flags.virtualCamera()) { + if (context.getDeviceId() == DEVICE_ID_DEFAULT) { return DEVICE_POLICY_DEFAULT; } @@ -1705,7 +1705,9 @@ public final class CameraManager { return ICameraService.ROTATION_OVERRIDE_NONE; } - if (context != null) { + // Isolated process does not have access to ActivityTaskManager service, which is used + // indirectly in `ActivityManager.getAppTasks()`. + if (context != null && !Process.isIsolated()) { final ActivityManager activityManager = context.getSystemService(ActivityManager.class); if (activityManager != null) { for (ActivityManager.AppTask appTask : activityManager.getAppTasks()) { @@ -2576,11 +2578,6 @@ public final class CameraManager { private boolean shouldHideCamera(int currentDeviceId, int devicePolicy, DeviceCameraInfo info) { - if (!android.companion.virtualdevice.flags.Flags.cameraDeviceAwareness()) { - // Don't hide any cameras if the device-awareness feature flag is disabled. - return false; - } - if (devicePolicy == DEVICE_POLICY_DEFAULT && info.mDeviceId == DEVICE_ID_DEFAULT) { // Don't hide default-device cameras for a default-policy virtual device. return false; diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java index fded88212127..d8919160320a 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -641,6 +641,9 @@ public final class DisplayManager { * is triggered whenever the properties of a {@link android.view.Display}, such as size, * state, density are modified. * + * This event is not triggered for refresh rate changes as they can change very often. + * To monitor refresh rate changes, subscribe to {@link EVENT_TYPE_DISPLAY_REFRESH_RATE}. + * * @see #registerDisplayListener(DisplayListener, Handler, long) * */ @@ -839,6 +842,9 @@ public final class DisplayManager { * Registers a display listener to receive notifications about when * displays are added, removed or changed. * + * We encourage to use {@link #registerDisplayListener(Executor, long, DisplayListener)} + * instead to subscribe for explicit events of interest + * * @param listener The listener to register. * @param handler The handler on which the listener should be invoked, or null * if the listener should be invoked on the calling thread's looper. @@ -847,7 +853,9 @@ public final class DisplayManager { */ public void registerDisplayListener(DisplayListener listener, Handler handler) { registerDisplayListener(listener, handler, EVENT_TYPE_DISPLAY_ADDED - | EVENT_TYPE_DISPLAY_CHANGED | EVENT_TYPE_DISPLAY_REMOVED); + | EVENT_TYPE_DISPLAY_CHANGED + | EVENT_TYPE_DISPLAY_REFRESH_RATE + | EVENT_TYPE_DISPLAY_REMOVED); } /** diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java index b5715ed25bd9..339dbf2c2029 100644 --- a/core/java/android/hardware/display/DisplayManagerGlobal.java +++ b/core/java/android/hardware/display/DisplayManagerGlobal.java @@ -1766,29 +1766,23 @@ public final class DisplayManagerGlobal { } if ((eventFlags & DisplayManager.EVENT_TYPE_DISPLAY_CHANGED) != 0) { - // For backward compatibility, a client subscribing to - // DisplayManager.EVENT_FLAG_DISPLAY_CHANGED will be enrolled to both Basic and - // RR changes - baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED - | INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE; + baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED; } - if ((eventFlags - & DisplayManager.EVENT_TYPE_DISPLAY_REMOVED) != 0) { + if ((eventFlags & DisplayManager.EVENT_TYPE_DISPLAY_REMOVED) != 0) { baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_REMOVED; } - if (Flags.displayListenerPerformanceImprovements()) { - if ((eventFlags & DisplayManager.EVENT_TYPE_DISPLAY_REFRESH_RATE) != 0) { - baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE; - } + if ((eventFlags & DisplayManager.EVENT_TYPE_DISPLAY_REFRESH_RATE) != 0) { + baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE; + } + if (Flags.displayListenerPerformanceImprovements()) { if ((eventFlags & DisplayManager.EVENT_TYPE_DISPLAY_STATE) != 0) { baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_STATE; } } - return baseEventMask; } } diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig index 23722ed5bb0d..8d58296e5581 100644 --- a/core/java/android/hardware/input/input_framework.aconfig +++ b/core/java/android/hardware/input/input_framework.aconfig @@ -233,3 +233,12 @@ flag { description: "Key Event Activity Detection" bug: "356412905" } + +flag { + name: "enable_backup_and_restore_for_input_gestures" + namespace: "input" + description: "Adds backup and restore support for custom input gestures" + bug: "382184249" + is_fixed_read_only: true +} + diff --git a/core/java/android/os/BaseBundle.java b/core/java/android/os/BaseBundle.java index e79b2e7becce..26044545b8d1 100644 --- a/core/java/android/os/BaseBundle.java +++ b/core/java/android/os/BaseBundle.java @@ -45,7 +45,8 @@ import java.util.function.BiFunction; * {@link PersistableBundle} subclass. */ @android.ravenwood.annotation.RavenwoodKeepWholeClass -public class BaseBundle { +@SuppressWarnings("HiddenSuperclass") +public class BaseBundle implements Parcel.ClassLoaderProvider { /** @hide */ protected static final String TAG = "Bundle"; static final boolean DEBUG = false; @@ -311,8 +312,9 @@ public class BaseBundle { /** * Return the ClassLoader currently associated with this Bundle. + * @hide */ - ClassLoader getClassLoader() { + public ClassLoader getClassLoader() { return mClassLoader; } @@ -426,6 +428,9 @@ public class BaseBundle { if ((mFlags & Bundle.FLAG_VERIFY_TOKENS_PRESENT) != 0) { Intent.maybeMarkAsMissingCreatorToken(object); } + } else if (object instanceof Bundle) { + Bundle bundle = (Bundle) object; + bundle.setClassLoaderSameAsContainerBundleWhenRetrievedFirstTime(this); } return (clazz != null) ? clazz.cast(object) : (T) object; } @@ -499,7 +504,7 @@ public class BaseBundle { int[] numLazyValues = new int[]{0}; try { parcelledData.readArrayMap(map, count, !parcelledByNative, - /* lazy */ ownsParcel, mClassLoader, numLazyValues); + /* lazy */ ownsParcel, this, numLazyValues); } catch (BadParcelableException e) { if (sShouldDefuse) { Log.w(TAG, "Failed to parse Bundle, but defusing quietly", e); diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java index a24dc5739b7e..c0591e6899b6 100644 --- a/core/java/android/os/Bundle.java +++ b/core/java/android/os/Bundle.java @@ -141,6 +141,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { STRIPPED.putInt("STRIPPED", 1); } + private boolean isFirstRetrievedFromABundle = false; + /** * Constructs a new, empty Bundle. */ @@ -382,7 +384,15 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { bundle.unparcel(); mOwnsLazyValues = false; bundle.mOwnsLazyValues = false; - mMap.putAll(bundle.mMap); + int N = bundle.mMap.size(); + for (int i = 0; i < N; i++) { + String key = bundle.mMap.keyAt(i); + Object value = bundle.mMap.valueAt(i); + if (value instanceof Bundle) { + ((Bundle) value).isFirstRetrievedFromABundle = true; + } + mMap.put(key, value); + } // FD and Binders state is now known if and only if both bundles already knew if ((bundle.mFlags & FLAG_HAS_FDS) != 0) { @@ -592,6 +602,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { mFlags &= ~FLAG_HAS_BINDERS_KNOWN; if (intentClass != null && intentClass.isInstance(value)) { setHasIntent(true); + } else if (value instanceof Bundle) { + ((Bundle) value).isFirstRetrievedFromABundle = true; } } @@ -793,6 +805,9 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { */ public void putBundle(@Nullable String key, @Nullable Bundle value) { unparcel(); + if (value != null) { + value.isFirstRetrievedFromABundle = true; + } mMap.put(key, value); } @@ -1020,7 +1035,9 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { return null; } try { - return (Bundle) o; + Bundle bundle = (Bundle) o; + bundle.setClassLoaderSameAsContainerBundleWhenRetrievedFirstTime(this); + return bundle; } catch (ClassCastException e) { typeWarning(key, o, "Bundle", e); return null; @@ -1028,6 +1045,21 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { } /** + * Set the ClassLoader of a bundle to its container bundle. This is necessary so that when a + * bundle's ClassLoader is changed, it can be propagated to its children. Do this only when it + * is retrieved from the container bundle first time though. Once it is accessed outside of its + * container, its ClassLoader should no longer be changed by its container anymore. + * + * @param containerBundle the bundle this bundle is retrieved from. + */ + void setClassLoaderSameAsContainerBundleWhenRetrievedFirstTime(BaseBundle containerBundle) { + if (!isFirstRetrievedFromABundle) { + setClassLoader(containerBundle.getClassLoader()); + isFirstRetrievedFromABundle = true; + } + } + + /** * Returns the value associated with the given key, or {@code null} if * no mapping of the desired type exists for the given key or a {@code null} * value is explicitly associated with the key. diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index e58934746c14..49d3f06eb80f 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -46,6 +46,7 @@ import android.util.SparseBooleanArray; import android.util.SparseIntArray; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import dalvik.annotation.optimization.CriticalNative; @@ -4661,7 +4662,7 @@ public final class Parcel { * @hide */ @Nullable - public Object readLazyValue(@Nullable ClassLoader loader) { + private Object readLazyValue(@Nullable ClassLoaderProvider loaderProvider) { int start = dataPosition(); int type = readInt(); if (isLengthPrefixed(type)) { @@ -4672,12 +4673,17 @@ public final class Parcel { int end = MathUtils.addOrThrow(dataPosition(), objectLength); int valueLength = end - start; setDataPosition(end); - return new LazyValue(this, start, valueLength, type, loader); + return new LazyValue(this, start, valueLength, type, loaderProvider); } else { - return readValue(type, loader, /* clazz */ null); + return readValue(type, getClassLoader(loaderProvider), /* clazz */ null); } } + @Nullable + private static ClassLoader getClassLoader(@Nullable ClassLoaderProvider loaderProvider) { + return loaderProvider == null ? null : loaderProvider.getClassLoader(); + } + private static final class LazyValue implements BiFunction<Class<?>, Class<?>[], Object> { /** @@ -4691,7 +4697,12 @@ public final class Parcel { private final int mPosition; private final int mLength; private final int mType; - @Nullable private final ClassLoader mLoader; + // this member is set when a bundle that includes a LazyValue is unparceled. But it is used + // when apply method is called. Between these 2 events, the bundle's ClassLoader could have + // changed. Let the bundle be a ClassLoaderProvider allows the bundle provides its current + // ClassLoader at the time apply method is called. + @NonNull + private final ClassLoaderProvider mLoaderProvider; @Nullable private Object mObject; /** @@ -4702,12 +4713,13 @@ public final class Parcel { */ @Nullable private volatile Parcel mSource; - LazyValue(Parcel source, int position, int length, int type, @Nullable ClassLoader loader) { + LazyValue(Parcel source, int position, int length, int type, + @NonNull ClassLoaderProvider loaderProvider) { mSource = requireNonNull(source); mPosition = position; mLength = length; mType = type; - mLoader = loader; + mLoaderProvider = loaderProvider; } @Override @@ -4720,7 +4732,8 @@ public final class Parcel { int restore = source.dataPosition(); try { source.setDataPosition(mPosition); - mObject = source.readValue(mLoader, clazz, itemTypes); + mObject = source.readValue(mLoaderProvider.getClassLoader(), clazz, + itemTypes); } finally { source.setDataPosition(restore); } @@ -4758,6 +4771,12 @@ public final class Parcel { return Parcel.hasFileDescriptors(mObject); } + /** @hide */ + @VisibleForTesting + public ClassLoader getClassLoader() { + return mLoaderProvider.getClassLoader(); + } + @Override public String toString() { return (mSource != null) @@ -4793,7 +4812,8 @@ public final class Parcel { return Objects.equals(mObject, value.mObject); } // Better safely fail here since this could mean we get different objects. - if (!Objects.equals(mLoader, value.mLoader)) { + if (!Objects.equals(mLoaderProvider.getClassLoader(), + value.mLoaderProvider.getClassLoader())) { return false; } // Otherwise compare metadata prior to comparing payload. @@ -4807,10 +4827,24 @@ public final class Parcel { @Override public int hashCode() { // Accessing mSource first to provide memory barrier for mObject - return Objects.hash(mSource == null, mObject, mLoader, mType, mLength); + return Objects.hash(mSource == null, mObject, mLoaderProvider.getClassLoader(), mType, + mLength); } } + /** + * Provides a ClassLoader. + * @hide + */ + public interface ClassLoaderProvider { + /** + * Returns a ClassLoader. + * + * @return ClassLoader + */ + ClassLoader getClassLoader(); + } + /** Same as {@link #readValue(ClassLoader, Class, Class[])} without any item types. */ private <T> T readValue(int type, @Nullable ClassLoader loader, @Nullable Class<T> clazz) { // Avoids allocating Class[0] array @@ -5551,8 +5585,8 @@ public final class Parcel { } private void readArrayMapInternal(@NonNull ArrayMap<? super String, Object> outVal, - int size, @Nullable ClassLoader loader) { - readArrayMap(outVal, size, /* sorted */ true, /* lazy */ false, loader, null); + int size, @Nullable ClassLoaderProvider loaderProvider) { + readArrayMap(outVal, size, /* sorted */ true, /* lazy */ false, loaderProvider, null); } /** @@ -5566,11 +5600,12 @@ public final class Parcel { * @hide */ void readArrayMap(ArrayMap<? super String, Object> map, int size, boolean sorted, - boolean lazy, @Nullable ClassLoader loader, int[] lazyValueCount) { + boolean lazy, @Nullable ClassLoaderProvider loaderProvider, int[] lazyValueCount) { ensureWithinMemoryLimit(SIZE_COMPLEX_TYPE, size); while (size > 0) { String key = readString(); - Object value = (lazy) ? readLazyValue(loader) : readValue(loader); + Object value = (lazy) ? readLazyValue(loaderProvider) : readValue( + getClassLoader(loaderProvider)); if (value instanceof LazyValue) { lazyValueCount[0]++; } @@ -5591,12 +5626,12 @@ public final class Parcel { */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public void readArrayMap(@NonNull ArrayMap<? super String, Object> outVal, - @Nullable ClassLoader loader) { + @Nullable ClassLoaderProvider loaderProvider) { final int N = readInt(); if (N < 0) { return; } - readArrayMapInternal(outVal, N, loader); + readArrayMapInternal(outVal, N, loaderProvider); } /** diff --git a/core/java/android/os/TestLooperManager.java b/core/java/android/os/TestLooperManager.java index 2d9d025b8d80..1a54f4df58fb 100644 --- a/core/java/android/os/TestLooperManager.java +++ b/core/java/android/os/TestLooperManager.java @@ -159,7 +159,7 @@ public class TestLooperManager { */ public void execute(Message message) { checkReleased(); - if (Looper.myLooper() == mLooper) { + if (mLooper.isCurrentThread()) { // This is being called from the thread it should be executed on, we can just dispatch. message.target.dispatchMessage(message); } else { diff --git a/core/java/android/os/health/SystemHealthManager.java b/core/java/android/os/health/SystemHealthManager.java index 9d0e221bd9e7..a8a22f675e08 100644 --- a/core/java/android/os/health/SystemHealthManager.java +++ b/core/java/android/os/health/SystemHealthManager.java @@ -473,17 +473,31 @@ public class SystemHealthManager { } } - final HealthStats[] results = new HealthStats[uids.length]; - if (result.bundle != null) { - HealthStatsParceler[] parcelers = result.bundle.getParcelableArray( - IBatteryStats.KEY_UID_SNAPSHOTS, HealthStatsParceler.class); - if (parcelers != null && parcelers.length == uids.length) { - for (int i = 0; i < parcelers.length; i++) { - results[i] = parcelers[i].getHealthStats(); + switch (result.resultCode) { + case IBatteryStats.RESULT_OK: { + final HealthStats[] results = new HealthStats[uids.length]; + if (result.bundle != null) { + HealthStatsParceler[] parcelers = result.bundle.getParcelableArray( + IBatteryStats.KEY_UID_SNAPSHOTS, HealthStatsParceler.class); + if (parcelers != null && parcelers.length == uids.length) { + for (int i = 0; i < parcelers.length; i++) { + results[i] = parcelers[i].getHealthStats(); + } + } } + return results; + } + case IBatteryStats.RESULT_SECURITY_EXCEPTION: { + throw new SecurityException(result.bundle != null + ? result.bundle.getString(IBatteryStats.KEY_EXCEPTION_MESSAGE) : null); + } + case IBatteryStats.RESULT_RUNTIME_EXCEPTION: { + throw new RuntimeException(result.bundle != null + ? result.bundle.getString(IBatteryStats.KEY_EXCEPTION_MESSAGE) : null); } + default: + throw new RuntimeException("Error code: " + result.resultCode); } - return results; } /** diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index 91ad22f51345..24f8672c1e7c 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -22,6 +22,7 @@ import static android.app.AppOpsManager.OP_LEGACY_STORAGE; import static android.app.AppOpsManager.OP_MANAGE_EXTERNAL_STORAGE; import static android.app.AppOpsManager.OP_READ_EXTERNAL_STORAGE; import static android.app.AppOpsManager.OP_READ_MEDIA_IMAGES; +import static android.app.PropertyInvalidatedCache.MODULE_SYSTEM; import static android.content.ContentResolver.DEPRECATE_DATA_PREFIX; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.UserHandle.PER_USER_RANGE; @@ -44,6 +45,7 @@ import android.app.ActivityThread; import android.app.AppGlobals; import android.app.AppOpsManager; import android.app.PendingIntent; +import android.app.PropertyInvalidatedCache; import android.compat.annotation.UnsupportedAppUsage; import android.content.ContentResolver; import android.content.Context; @@ -269,14 +271,15 @@ public class StorageManager { public static final int FLAG_STORAGE_SDK = IInstalld.FLAG_STORAGE_SDK; /** {@hide} */ - @IntDef(prefix = "FLAG_STORAGE_", value = { + @IntDef(prefix = "FLAG_STORAGE_", value = { FLAG_STORAGE_DE, FLAG_STORAGE_CE, FLAG_STORAGE_EXTERNAL, FLAG_STORAGE_SDK, }) @Retention(RetentionPolicy.SOURCE) - public @interface StorageFlags {} + public @interface StorageFlags { + } /** {@hide} */ public static final int FLAG_FOR_WRITE = 1 << 8; @@ -309,6 +312,44 @@ public class StorageManager { @GuardedBy("mDelegates") private final ArrayList<StorageEventListenerDelegate> mDelegates = new ArrayList<>(); + static record VolumeListQuery(int mUserId, String mPackageName, int mFlags) { + } + + private static final PropertyInvalidatedCache.QueryHandler<VolumeListQuery, StorageVolume[]> + sVolumeListQuery = new PropertyInvalidatedCache.QueryHandler<>() { + @androidx.annotation.Nullable + @Override + public StorageVolume[] apply(@androidx.annotation.NonNull VolumeListQuery query) { + final IStorageManager storageManager = IStorageManager.Stub.asInterface( + ServiceManager.getService("mount")); + if (storageManager == null) { + // negative results won't be cached, so we will just try again next time + return null; + } + try { + return storageManager.getVolumeList( + query.mUserId, query.mPackageName, query.mFlags); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + }; + + // Generally, the userId and packageName parameters stay pretty constant, but flags may change + // regularly; we have observed some processes hitting 10+ variations. + private static final int VOLUME_LIST_CACHE_MAX = 16; + + private static final PropertyInvalidatedCache<VolumeListQuery, StorageVolume[]> + sVolumeListCache = new PropertyInvalidatedCache<>( + new PropertyInvalidatedCache.Args(MODULE_SYSTEM).cacheNulls(false) + .api("getVolumeList").maxEntries(VOLUME_LIST_CACHE_MAX), "getVolumeList", + sVolumeListQuery); + + /** {@hide} */ + public static void invalidateVolumeListCache() { + sVolumeListCache.invalidateCache(); + } + private class StorageEventListenerDelegate extends IStorageEventListener.Stub { final Executor mExecutor; final StorageEventListener mListener; @@ -395,7 +436,8 @@ public class StorageManager { private class ObbActionListener extends IObbActionListener.Stub { @SuppressWarnings("hiding") - private SparseArray<ObbListenerDelegate> mListeners = new SparseArray<ObbListenerDelegate>(); + private SparseArray<ObbListenerDelegate> mListeners = + new SparseArray<ObbListenerDelegate>(); @Override public void onObbResult(String filename, int nonce, int status) { @@ -477,10 +519,10 @@ public class StorageManager { * * @param looper The {@link android.os.Looper} which events will be received on. * - * <p>Applications can get instance of this class by calling - * {@link android.content.Context#getSystemService(java.lang.String)} with an argument - * of {@link android.content.Context#STORAGE_SERVICE}. - * + * <p>Applications can get instance of this class by calling + * {@link android.content.Context#getSystemService(java.lang.String)} with an + * argument + * of {@link android.content.Context#STORAGE_SERVICE}. * @hide */ @UnsupportedAppUsage @@ -488,15 +530,16 @@ public class StorageManager { mContext = context; mResolver = context.getContentResolver(); mLooper = looper; - mStorageManager = IStorageManager.Stub.asInterface(ServiceManager.getServiceOrThrow("mount")); + mStorageManager = IStorageManager.Stub.asInterface( + ServiceManager.getServiceOrThrow("mount")); mAppOps = mContext.getSystemService(AppOpsManager.class); } /** * Registers a {@link android.os.storage.StorageEventListener StorageEventListener}. * - * @param listener A {@link android.os.storage.StorageEventListener StorageEventListener} object. - * + * @param listener A {@link android.os.storage.StorageEventListener StorageEventListener} + * object. * @hide */ @UnsupportedAppUsage @@ -516,14 +559,14 @@ public class StorageManager { /** * Unregisters a {@link android.os.storage.StorageEventListener StorageEventListener}. * - * @param listener A {@link android.os.storage.StorageEventListener StorageEventListener} object. - * + * @param listener A {@link android.os.storage.StorageEventListener StorageEventListener} + * object. * @hide */ @UnsupportedAppUsage public void unregisterListener(StorageEventListener listener) { synchronized (mDelegates) { - for (Iterator<StorageEventListenerDelegate> i = mDelegates.iterator(); i.hasNext();) { + for (Iterator<StorageEventListenerDelegate> i = mDelegates.iterator(); i.hasNext(); ) { final StorageEventListenerDelegate delegate = i.next(); if (delegate.mListener == listener) { try { @@ -558,7 +601,8 @@ public class StorageManager { * {@link StorageManager#getStorageVolumes()} to observe the latest * value. */ - public void onStateChanged(@NonNull StorageVolume volume) { } + public void onStateChanged(@NonNull StorageVolume volume) { + } } /** @@ -592,7 +636,7 @@ public class StorageManager { */ public void unregisterStorageVolumeCallback(@NonNull StorageVolumeCallback callback) { synchronized (mDelegates) { - for (Iterator<StorageEventListenerDelegate> i = mDelegates.iterator(); i.hasNext();) { + for (Iterator<StorageEventListenerDelegate> i = mDelegates.iterator(); i.hasNext(); ) { final StorageEventListenerDelegate delegate = i.next(); if (delegate.mCallback == callback) { try { @@ -628,8 +672,8 @@ public class StorageManager { /** * Query if a USB Mass Storage (UMS) host is connected. - * @return true if UMS host is connected. * + * @return true if UMS host is connected. * @hide */ @Deprecated @@ -640,8 +684,8 @@ public class StorageManager { /** * Query if a USB Mass Storage (UMS) is enabled on the device. - * @return true if UMS host is enabled. * + * @return true if UMS host is enabled. * @hide */ @Deprecated @@ -663,11 +707,11 @@ public class StorageManager { * That is, shared UID applications can attempt to mount any other * application's OBB that shares its UID. * - * @param rawPath the path to the OBB file - * @param key must be <code>null</code>. Previously, some Android device - * implementations accepted a non-<code>null</code> key to mount - * an encrypted OBB file. However, this never worked reliably and - * is no longer supported. + * @param rawPath the path to the OBB file + * @param key must be <code>null</code>. Previously, some Android device + * implementations accepted a non-<code>null</code> key to mount + * an encrypted OBB file. However, this never worked reliably and + * is no longer supported. * @param listener will receive the success or failure of the operation * @return whether the mount call was successfully queued or not */ @@ -739,9 +783,9 @@ public class StorageManager { * application's OBB that shares its UID. * <p> * - * @param rawPath path to the OBB file - * @param force whether to kill any programs using this in order to unmount - * it + * @param rawPath path to the OBB file + * @param force whether to kill any programs using this in order to unmount + * it * @param listener will receive the success or failure of the operation * @return whether the unmount call was successfully queued or not */ @@ -781,7 +825,7 @@ public class StorageManager { * * @param rawPath path to OBB image * @return absolute path to mounted OBB image data or <code>null</code> if - * not mounted or exception encountered trying to read status + * not mounted or exception encountered trying to read status */ public String getMountedObbPath(String rawPath) { Preconditions.checkNotNull(rawPath, "rawPath cannot be null"); @@ -899,7 +943,7 @@ public class StorageManager { * {@link #UUID_DEFAULT}. * * @throws IOException when the storage device hosting the given path isn't - * present, or when it doesn't have a valid UUID. + * present, or when it doesn't have a valid UUID. */ public @NonNull UUID getUuidForPath(@NonNull File path) throws IOException { Preconditions.checkNotNull(path); @@ -1172,8 +1216,8 @@ public class StorageManager { /** * This is not the API you're looking for. * - * @see PackageManager#getPrimaryStorageCurrentVolume() * @hide + * @see PackageManager#getPrimaryStorageCurrentVolume() */ public String getPrimaryStorageUuid() { try { @@ -1186,8 +1230,8 @@ public class StorageManager { /** * This is not the API you're looking for. * - * @see PackageManager#movePrimaryStorage(VolumeInfo) * @hide + * @see PackageManager#movePrimaryStorage(VolumeInfo) */ public void setPrimaryStorageUuid(String volumeUuid, IPackageMoveObserver callback) { try { @@ -1216,7 +1260,7 @@ public class StorageManager { // resolve the actual volume name if (Objects.equals(volumeName, MediaStore.VOLUME_EXTERNAL)) { try (Cursor c = mContext.getContentResolver().query(uri, - new String[] { MediaStore.MediaColumns.VOLUME_NAME }, null, null)) { + new String[]{MediaStore.MediaColumns.VOLUME_NAME}, null, null)) { if (c.moveToFirst()) { volumeName = c.getString(0); } @@ -1275,6 +1319,7 @@ public class StorageManager { /** * Gets the state of a volume via its mountpoint. + * * @hide */ @Deprecated @@ -1308,7 +1353,7 @@ public class StorageManager { * Return the list of shared/external storage volumes currently available to * the calling user and the user it shares media with. Please refer to * <a href="https://source.android.com/compatibility/12/android-12-cdd#95_multi-user_support"> - * multi-user support</a> for more details. + * multi-user support</a> for more details. * * <p> * This is similar to {@link StorageManager#getStorageVolumes()} except that the result also @@ -1353,7 +1398,7 @@ public class StorageManager { public static Pair<String, Long> getPrimaryStoragePathAndSize() { return Pair.create(null, FileUtils.roundStorageSize(Environment.getDataDirectory().getTotalSpace() - + Environment.getRootDirectory().getTotalSpace())); + + Environment.getRootDirectory().getTotalSpace())); } /** {@hide} */ @@ -1389,8 +1434,6 @@ public class StorageManager { /** {@hide} */ @UnsupportedAppUsage public static @NonNull StorageVolume[] getVolumeList(int userId, int flags) { - final IStorageManager storageManager = IStorageManager.Stub.asInterface( - ServiceManager.getService("mount")); try { String packageName = ActivityThread.currentOpPackageName(); if (packageName == null) { @@ -1406,7 +1449,7 @@ public class StorageManager { } packageName = packageNames[0]; } - return storageManager.getVolumeList(userId, packageName, flags); + return sVolumeListCache.query(new VolumeListQuery(userId, packageName, flags)); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1414,6 +1457,7 @@ public class StorageManager { /** * Returns list of paths for all mountable volumes. + * * @hide */ @Deprecated @@ -1605,7 +1649,7 @@ public class StorageManager { * <p> * This is only intended to be called by UserManagerService, as part of creating a user. * - * @param userId ID of the user + * @param userId ID of the user * @param ephemeral whether the user is ephemeral * @throws RuntimeException on error. The user's keys already existing is considered an error. * @hide @@ -1711,7 +1755,8 @@ public class StorageManager { return false; } - /** {@hide} + /** + * {@hide} * Is this device encrypted? * <p> * Note: all devices launching with Android 10 (API level 29) or later are @@ -1724,8 +1769,10 @@ public class StorageManager { return RoSystemProperties.CRYPTO_ENCRYPTED; } - /** {@hide} + /** + * {@hide} * Does this device have file-based encryption (FBE) enabled? + * * @return true if the device has file-based encryption enabled. */ public static boolean isFileEncrypted() { @@ -1759,8 +1806,8 @@ public class StorageManager { } /** - * @deprecated disabled now that FUSE has been replaced by sdcardfs * @hide + * @deprecated disabled now that FUSE has been replaced by sdcardfs */ @Deprecated public static File maybeTranslateEmulatedPathToInternal(File path) { @@ -1790,6 +1837,7 @@ public class StorageManager { /** * Check that given app holds both permission and appop. + * * @hide */ public static boolean checkPermissionAndAppOp(Context context, boolean enforce, int pid, @@ -1800,6 +1848,7 @@ public class StorageManager { /** * Check that given app holds both permission and appop but do not noteOp. + * * @hide */ public static boolean checkPermissionAndCheckOp(Context context, boolean enforce, @@ -1810,6 +1859,7 @@ public class StorageManager { /** * Check that given app holds both permission and appop. + * * @hide */ private static boolean checkPermissionAndAppOp(Context context, boolean enforce, int pid, @@ -1877,7 +1927,9 @@ public class StorageManager { // Legacy apps technically have the access granted by this op, // even when the op is denied if ((mAppOps.checkOpNoThrow(OP_LEGACY_STORAGE, uid, - packageName) == AppOpsManager.MODE_ALLOWED)) return true; + packageName) == AppOpsManager.MODE_ALLOWED)) { + return true; + } if (enforce) { throw new SecurityException("Op " + AppOpsManager.opToName(op) + " " @@ -1924,7 +1976,7 @@ public class StorageManager { return true; } if (mode == AppOpsManager.MODE_DEFAULT && mContext.checkPermission( - MANAGE_EXTERNAL_STORAGE, pid, uid) == PERMISSION_GRANTED) { + MANAGE_EXTERNAL_STORAGE, pid, uid) == PERMISSION_GRANTED) { return true; } // If app doesn't have MANAGE_EXTERNAL_STORAGE, then check if it has requested granular @@ -1936,7 +1988,7 @@ public class StorageManager { @VisibleForTesting public @NonNull ParcelFileDescriptor openProxyFileDescriptor( int mode, ProxyFileDescriptorCallback callback, Handler handler, ThreadFactory factory) - throws IOException { + throws IOException { Preconditions.checkNotNull(callback); MetricsLogger.count(mContext, "storage_open_proxy_file_descriptor", 1); // Retry is needed because the mount point mFuseAppLoop is using may be unmounted before @@ -1987,7 +2039,7 @@ public class StorageManager { /** {@hide} */ public @NonNull ParcelFileDescriptor openProxyFileDescriptor( int mode, ProxyFileDescriptorCallback callback) - throws IOException { + throws IOException { return openProxyFileDescriptor(mode, callback, null, null); } @@ -2006,19 +2058,18 @@ public class StorageManager { * you're willing to decrypt on-demand, but where you want to avoid * persisting the cleartext version. * - * @param mode The desired access mode, must be one of - * {@link ParcelFileDescriptor#MODE_READ_ONLY}, - * {@link ParcelFileDescriptor#MODE_WRITE_ONLY}, or - * {@link ParcelFileDescriptor#MODE_READ_WRITE} + * @param mode The desired access mode, must be one of + * {@link ParcelFileDescriptor#MODE_READ_ONLY}, + * {@link ParcelFileDescriptor#MODE_WRITE_ONLY}, or + * {@link ParcelFileDescriptor#MODE_READ_WRITE} * @param callback Callback to process file operation requests issued on - * returned file descriptor. - * @param handler Handler that invokes callback methods. + * returned file descriptor. + * @param handler Handler that invokes callback methods. * @return Seekable ParcelFileDescriptor. - * @throws IOException */ public @NonNull ParcelFileDescriptor openProxyFileDescriptor( int mode, ProxyFileDescriptorCallback callback, Handler handler) - throws IOException { + throws IOException { Preconditions.checkNotNull(handler); return openProxyFileDescriptor(mode, callback, handler, null); } @@ -2050,10 +2101,10 @@ public class StorageManager { * </p> * * @param storageUuid the UUID of the storage volume that you're interested - * in. The UUID for a specific path can be obtained using - * {@link #getUuidForPath(File)}. + * in. The UUID for a specific path can be obtained using + * {@link #getUuidForPath(File)}. * @throws IOException when the storage device isn't present, or when it - * doesn't support cache quotas. + * doesn't support cache quotas. * @see #getCacheSizeBytes(UUID) */ @WorkerThread @@ -2085,10 +2136,10 @@ public class StorageManager { * </p> * * @param storageUuid the UUID of the storage volume that you're interested - * in. The UUID for a specific path can be obtained using - * {@link #getUuidForPath(File)}. + * in. The UUID for a specific path can be obtained using + * {@link #getUuidForPath(File)}. * @throws IOException when the storage device isn't present, or when it - * doesn't support cache quotas. + * doesn't support cache quotas. * @see #getCacheQuotaBytes(UUID) */ @WorkerThread @@ -2106,7 +2157,7 @@ public class StorageManager { /** @hide */ - @IntDef(prefix = { "MOUNT_MODE_" }, value = { + @IntDef(prefix = {"MOUNT_MODE_"}, value = { MOUNT_MODE_EXTERNAL_NONE, MOUNT_MODE_EXTERNAL_DEFAULT, MOUNT_MODE_EXTERNAL_INSTALLER, @@ -2115,16 +2166,19 @@ public class StorageManager { }) @Retention(RetentionPolicy.SOURCE) /** @hide */ - public @interface MountMode {} + public @interface MountMode { + } /** * No external storage should be mounted. + * * @hide */ @SystemApi public static final int MOUNT_MODE_EXTERNAL_NONE = IVold.REMOUNT_MODE_NONE; /** * Default external storage should be mounted. + * * @hide */ @SystemApi @@ -2132,12 +2186,14 @@ public class StorageManager { /** * Mount mode for package installers which should give them access to * all obb dirs in addition to their package sandboxes + * * @hide */ @SystemApi public static final int MOUNT_MODE_EXTERNAL_INSTALLER = IVold.REMOUNT_MODE_INSTALLER; /** * The lower file system should be bind mounted directly on external storage + * * @hide */ @SystemApi @@ -2146,6 +2202,7 @@ public class StorageManager { /** * Use the regular scoped storage filesystem, but Android/ should be writable. * Used to support the applications hosting DownloadManager and the MTP server. + * * @hide */ @SystemApi @@ -2164,10 +2221,10 @@ public class StorageManager { * this flag to take effect. * </p> * + * @hide * @see #getAllocatableBytes(UUID, int) * @see #allocateBytes(UUID, long, int) * @see #allocateBytes(FileDescriptor, long, int) - * @hide */ @RequiresPermission(android.Manifest.permission.ALLOCATE_AGGRESSIVE) @SystemApi @@ -2194,6 +2251,7 @@ public class StorageManager { * freeable cached space when determining allocatable space. * * Intended for use with {@link #getAllocatableBytes()}. + * * @hide */ public static final int FLAG_ALLOCATE_NON_CACHE_ONLY = 1 << 3; @@ -2203,12 +2261,13 @@ public class StorageManager { * cached space when determining allocatable space. * * Intended for use with {@link #getAllocatableBytes()}. + * * @hide */ public static final int FLAG_ALLOCATE_CACHE_ONLY = 1 << 4; /** @hide */ - @IntDef(flag = true, prefix = { "FLAG_ALLOCATE_" }, value = { + @IntDef(flag = true, prefix = {"FLAG_ALLOCATE_"}, value = { FLAG_ALLOCATE_AGGRESSIVE, FLAG_ALLOCATE_DEFY_ALL_RESERVED, FLAG_ALLOCATE_DEFY_HALF_RESERVED, @@ -2216,7 +2275,8 @@ public class StorageManager { FLAG_ALLOCATE_CACHE_ONLY, }) @Retention(RetentionPolicy.SOURCE) - public @interface AllocateFlags {} + public @interface AllocateFlags { + } /** * Return the maximum number of new bytes that your app can allocate for @@ -2246,15 +2306,15 @@ public class StorageManager { * </p> * * @param storageUuid the UUID of the storage volume where you're - * considering allocating disk space, since allocatable space can - * vary widely depending on the underlying storage device. The - * UUID for a specific path can be obtained using - * {@link #getUuidForPath(File)}. + * considering allocating disk space, since allocatable space can + * vary widely depending on the underlying storage device. The + * UUID for a specific path can be obtained using + * {@link #getUuidForPath(File)}. * @return the maximum number of new bytes that the calling app can allocate - * using {@link #allocateBytes(UUID, long)} or - * {@link #allocateBytes(FileDescriptor, long)}. + * using {@link #allocateBytes(UUID, long)} or + * {@link #allocateBytes(FileDescriptor, long)}. * @throws IOException when the storage device isn't present, or when it - * doesn't support allocating space. + * doesn't support allocating space. */ @WorkerThread public @BytesLong long getAllocatableBytes(@NonNull UUID storageUuid) @@ -2297,12 +2357,12 @@ public class StorageManager { * more than once every 60 seconds. * * @param storageUuid the UUID of the storage volume where you'd like to - * allocate disk space. The UUID for a specific path can be - * obtained using {@link #getUuidForPath(File)}. - * @param bytes the number of bytes to allocate. + * allocate disk space. The UUID for a specific path can be + * obtained using {@link #getUuidForPath(File)}. + * @param bytes the number of bytes to allocate. * @throws IOException when the storage device isn't present, or when it - * doesn't support allocating space, or if the device had - * trouble allocating the requested space. + * doesn't support allocating space, or if the device had + * trouble allocating the requested space. * @see #getAllocatableBytes(UUID) */ @WorkerThread @@ -2332,10 +2392,9 @@ public class StorageManager { * These mount modes specify different views and access levels for * different apps on external storage. * + * @return {@code MountMode} for the given uid and packageName. * @params uid UID of the application * @params packageName name of the package - * @return {@code MountMode} for the given uid and packageName. - * * @hide */ @RequiresPermission(android.Manifest.permission.WRITE_MEDIA_STORAGE) @@ -2366,15 +2425,15 @@ public class StorageManager { * (such as when recording a video) you should avoid calling this method * more than once every 60 seconds. * - * @param fd the open file that you'd like to allocate disk space for. + * @param fd the open file that you'd like to allocate disk space for. * @param bytes the number of bytes to allocate. This is the desired final - * size of the open file. If the open file is smaller than this - * requested size, it will be extended without modifying any - * existing contents. If the open file is larger than this - * requested size, it will be truncated. + * size of the open file. If the open file is smaller than this + * requested size, it will be extended without modifying any + * existing contents. If the open file is larger than this + * requested size, it will be truncated. * @throws IOException when the storage device isn't present, or when it - * doesn't support allocating space, or if the device had - * trouble allocating the requested space. + * doesn't support allocating space, or if the device had + * trouble allocating the requested space. * @see #isAllocationSupported(FileDescriptor) * @see Environment#isExternalStorageEmulated(File) */ @@ -2499,13 +2558,14 @@ public class StorageManager { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef(prefix = { "QUOTA_TYPE_" }, value = { + @IntDef(prefix = {"QUOTA_TYPE_"}, value = { QUOTA_TYPE_MEDIA_NONE, QUOTA_TYPE_MEDIA_AUDIO, QUOTA_TYPE_MEDIA_VIDEO, QUOTA_TYPE_MEDIA_IMAGE, }) - public @interface QuotaType {} + public @interface QuotaType { + } private static native boolean setQuotaProjectId(String path, long projectId); @@ -2532,15 +2592,13 @@ public class StorageManager { * The default platform user of this API is the MediaProvider process, which is * responsible for managing all of external storage. * - * @param path the path to the file for which we should update the quota type + * @param path the path to the file for which we should update the quota type * @param quotaType the quota type of the file; this is based on the * {@code QuotaType} constants, eg * {@code StorageManager.QUOTA_TYPE_MEDIA_AUDIO} - * * @throws IllegalArgumentException if {@code quotaType} does not correspond to a valid * quota type. * @throws IOException if the quota type could not be updated. - * * @hide */ @SystemApi @@ -2616,7 +2674,6 @@ public class StorageManager { * permissions of a directory to what they should anyway be. * * @param path the path for which we should fix up the permissions - * * @hide */ public void fixupAppDir(@NonNull File path) { @@ -2822,11 +2879,12 @@ public class StorageManager { * @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef(prefix = { "APP_IO_BLOCKED_REASON_" }, value = { - APP_IO_BLOCKED_REASON_TRANSCODING, - APP_IO_BLOCKED_REASON_UNKNOWN, + @IntDef(prefix = {"APP_IO_BLOCKED_REASON_"}, value = { + APP_IO_BLOCKED_REASON_TRANSCODING, + APP_IO_BLOCKED_REASON_UNKNOWN, }) - public @interface AppIoBlockedReason {} + public @interface AppIoBlockedReason { + } /** * Notify the system that an app with {@code uid} and {@code tid} is blocked on an IO request on @@ -2839,10 +2897,9 @@ public class StorageManager { * {@link android.Manifest.permission#WRITE_MEDIA_STORAGE} permission. * * @param volumeUuid the UUID of the storage volume that the app IO is blocked on - * @param uid the UID of the app blocked on IO - * @param tid the tid of the app blocked on IO - * @param reason the reason the app is blocked on IO - * + * @param uid the UID of the app blocked on IO + * @param tid the tid of the app blocked on IO + * @param reason the reason the app is blocked on IO * @hide */ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) @@ -2866,10 +2923,9 @@ public class StorageManager { * {@link android.Manifest.permission#WRITE_MEDIA_STORAGE} permission. * * @param volumeUuid the UUID of the storage volume that the app IO is resumed on - * @param uid the UID of the app resuming IO - * @param tid the tid of the app resuming IO - * @param reason the reason the app is resuming IO - * + * @param uid the UID of the app resuming IO + * @param tid the tid of the app resuming IO + * @param reason the reason the app is resuming IO * @hide */ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) @@ -2890,10 +2946,9 @@ public class StorageManager { * {@link android.Manifest.permission#WRITE_MEDIA_STORAGE} permission. * * @param volumeUuid the UUID of the storage volume to check IO blocked status - * @param uid the UID of the app to check IO blocked status - * @param tid the tid of the app to check IO blocked status - * @param reason the reason to check IO blocked status for - * + * @param uid the UID of the app to check IO blocked status + * @param tid the tid of the app to check IO blocked status + * @param reason the reason to check IO blocked status for * @hide */ @TestApi @@ -2962,7 +3017,6 @@ public class StorageManager { * information is available, -1 is returned. * * @return Percentage of the remaining useful lifetime of the internal storage device. - * * @hide */ @FlaggedApi(Flags.FLAG_STORAGE_LIFETIME_API) diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index a480a3b013bb..daa5584462ba 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -499,3 +499,11 @@ flag { description: "Collect sqlite performance metrics for discrete ops." bug: "377584611" } + +flag { + name: "app_ops_service_handler_fix" + is_fixed_read_only: true + namespace: "permissions" + description: "Use IoThread handler for AppOpsService background/IO work." + bug: "394380603" +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 3cd7a00591ca..f1a9514107da 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -13006,6 +13006,24 @@ public final class Settings { public static final String STYLUS_POINTER_ICON_ENABLED = "stylus_pointer_icon_enabled"; /** + * Toggle for whether to redact OTP notification while connected to wifi. Defaults to + * false/0. + * @hide + */ + @Readable + public static final String REDACT_OTP_NOTIFICATION_WHILE_CONNECTED_TO_WIFI = + "redact_otp_on_wifi"; + + /** + * Toggle for whether to immediately redact OTP notifications, or require the device to be + * locked for 10 minutes. Defaults to false/0 + * @hide + */ + @Readable + public static final String REDACT_OTP_NOTIFICATION_IMMEDIATELY = + "remove_otp_redaction_delay"; + + /** * These entries are considered common between the personal and the managed profile, * since the managed profile doesn't get to change them. */ diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java index 6eaef78ff608..f7f4eeca58e2 100644 --- a/core/java/android/provider/Telephony.java +++ b/core/java/android/provider/Telephony.java @@ -17,7 +17,6 @@ package android.provider; import android.Manifest; -import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.RequiresPermission; @@ -50,7 +49,6 @@ import android.text.TextUtils; import android.util.Patterns; import com.android.internal.telephony.SmsApplication; -import com.android.internal.telephony.flags.Flags; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -3196,7 +3194,6 @@ public final class Telephony { * See 3GPP TS 23.501 section 5.6.13 * <P>Type: INTEGER</P> */ - @FlaggedApi(Flags.FLAG_APN_SETTING_FIELD_SUPPORT_FLAG) public static final String ALWAYS_ON = "always_on"; /** @@ -3307,7 +3304,6 @@ public final class Telephony { * connected, in bytes. * <p>Type: INTEGER </p> */ - @FlaggedApi(Flags.FLAG_APN_SETTING_FIELD_SUPPORT_FLAG) public static final String MTU_V4 = "mtu_v4"; /** @@ -3315,7 +3311,6 @@ public final class Telephony { * connected, in bytes. * <p>Type: INTEGER </p> */ - @FlaggedApi(Flags.FLAG_APN_SETTING_FIELD_SUPPORT_FLAG) public static final String MTU_V6 = "mtu_v6"; /** @@ -3338,14 +3333,12 @@ public final class Telephony { * {@code true} if this APN visible to the user, {@code false} otherwise. * <p>Type: INTEGER (boolean)</p> */ - @FlaggedApi(Flags.FLAG_APN_SETTING_FIELD_SUPPORT_FLAG) public static final String USER_VISIBLE = "user_visible"; /** * {@code true} if the user allowed to edit this APN, {@code false} otherwise. * <p>Type: INTEGER (boolean)</p> */ - @FlaggedApi(Flags.FLAG_APN_SETTING_FIELD_SUPPORT_FLAG) public static final String USER_EDITABLE = "user_editable"; /** diff --git a/core/java/android/view/InputEventConsistencyVerifier.java b/core/java/android/view/InputEventConsistencyVerifier.java index 195896dc8edf..0e78bfdb5069 100644 --- a/core/java/android/view/InputEventConsistencyVerifier.java +++ b/core/java/android/view/InputEventConsistencyVerifier.java @@ -180,7 +180,7 @@ public final class InputEventConsistencyVerifier { final MotionEvent motionEvent = (MotionEvent)event; if (motionEvent.isTouchEvent()) { onTouchEvent(motionEvent, nestingLevel); - } else if ((motionEvent.getSource() & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { + } else if (motionEvent.isFromSource(InputDevice.SOURCE_TRACKBALL)) { onTrackballEvent(motionEvent, nestingLevel); } else { onGenericMotionEvent(motionEvent, nestingLevel); diff --git a/core/java/android/view/InputEventReceiver.java b/core/java/android/view/InputEventReceiver.java index 1c36eaf99afa..9c1f134bff3e 100644 --- a/core/java/android/view/InputEventReceiver.java +++ b/core/java/android/view/InputEventReceiver.java @@ -290,9 +290,15 @@ public abstract class InputEventReceiver { @SuppressWarnings("unused") @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private void dispatchInputEvent(int seq, InputEvent event) { - Trace.traceBegin(Trace.TRACE_TAG_INPUT, "dispatchInputEvent " + getShortDescription(event)); + if (Trace.isTagEnabled(Trace.TRACE_TAG_INPUT)) { + // This 'if' block is an optimization - without it, 'getShortDescription' will be + // called unconditionally, which is expensive. + Trace.traceBegin(Trace.TRACE_TAG_INPUT, + "dispatchInputEvent " + getShortDescription(event)); + } mSeqMap.put(event.getSequenceNumber(), seq); onInputEvent(event); + // If tracing is not enabled, `traceEnd` is a no-op (so we don't need to guard it with 'if') Trace.traceEnd(Trace.TRACE_TAG_INPUT); } diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java index e8e66210bca6..945975a88cd5 100644 --- a/core/java/android/view/InsetsSourceConsumer.java +++ b/core/java/android/view/InsetsSourceConsumer.java @@ -32,6 +32,7 @@ import static com.android.internal.annotations.VisibleForTesting.Visibility.PACK import android.annotation.IntDef; import android.annotation.Nullable; +import android.graphics.Insets; import android.graphics.Matrix; import android.graphics.Point; import android.graphics.Rect; @@ -168,8 +169,9 @@ public class InsetsSourceConsumer { // Reset the applier to the default one which has the most lightweight implementation. setSurfaceParamsApplier(InsetsAnimationControlRunner.SurfaceParamsApplier.DEFAULT); } else { - if (lastControl != null && InsetsSource.getInsetSide(lastControl.getInsetsHint()) - != InsetsSource.getInsetSide(control.getInsetsHint())) { + if (lastControl != null && !Insets.NONE.equals(lastControl.getInsetsHint()) + && InsetsSource.getInsetSide(lastControl.getInsetsHint()) + != InsetsSource.getInsetSide(control.getInsetsHint())) { // The source has been moved to a different side. The coordinates are stale. // Canceling existing animation if there is any. cancelTypes[0] |= mType; diff --git a/core/java/android/view/RoundScrollbarRenderer.java b/core/java/android/view/RoundScrollbarRenderer.java index 5e1eadae0953..331e34526ae8 100644 --- a/core/java/android/view/RoundScrollbarRenderer.java +++ b/core/java/android/view/RoundScrollbarRenderer.java @@ -20,6 +20,7 @@ import static android.util.MathUtils.acos; import static java.lang.Math.sin; +import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; @@ -40,9 +41,9 @@ public class RoundScrollbarRenderer { // The range of the scrollbar position represented as an angle in degrees. private static final float SCROLLBAR_ANGLE_RANGE = 28.8f; - private static final float MAX_SCROLLBAR_ANGLE_SWIPE = 26.3f; // 90% - private static final float MIN_SCROLLBAR_ANGLE_SWIPE = 3.1f; // 10% - private static final float THUMB_WIDTH_DP = 4f; + private static final float MAX_SCROLLBAR_ANGLE_SWIPE = 0.7f * SCROLLBAR_ANGLE_RANGE; + private static final float MIN_SCROLLBAR_ANGLE_SWIPE = 0.3f * SCROLLBAR_ANGLE_RANGE; + private static final float GAP_BETWEEN_TRACK_AND_THUMB_DP = 3f; private static final float OUTER_PADDING_DP = 2f; private static final int DEFAULT_THUMB_COLOR = 0xFFFFFFFF; private static final int DEFAULT_TRACK_COLOR = 0x4CFFFFFF; @@ -57,14 +58,16 @@ public class RoundScrollbarRenderer { private final RectF mRect = new RectF(); private final View mParent; private final float mInset; + private final float mGapBetweenThumbAndTrackPx; + private final boolean mUseRefactoredRoundScrollbar; private float mPreviousMaxScroll = 0; private float mMaxScrollDiff = 0; private float mPreviousCurrentScroll = 0; private float mCurrentScrollDiff = 0; private float mThumbStrokeWidthAsDegrees = 0; + private float mGapBetweenTrackAndThumbAsDegrees = 0; private boolean mDrawToLeft; - private boolean mUseRefactoredRoundScrollbar; public RoundScrollbarRenderer(View parent) { // Paints for the round scrollbar. @@ -80,16 +83,17 @@ public class RoundScrollbarRenderer { mParent = parent; + Resources resources = parent.getContext().getResources(); // Fetch the resource indicating the thickness of CircularDisplayMask, rounding in the same // way WindowManagerService.showCircularMask does. The scroll bar is inset by this amount so // that it doesn't get clipped. int maskThickness = - parent.getContext() - .getResources() - .getDimensionPixelSize( - com.android.internal.R.dimen.circular_display_mask_thickness); + resources.getDimensionPixelSize( + com.android.internal.R.dimen.circular_display_mask_thickness); - float thumbWidth = dpToPx(THUMB_WIDTH_DP); + float thumbWidth = + resources.getDimensionPixelSize(com.android.internal.R.dimen.round_scrollbar_width); + mGapBetweenThumbAndTrackPx = dpToPx(GAP_BETWEEN_TRACK_AND_THUMB_DP); mThumbPaint.setStrokeWidth(thumbWidth); mTrackPaint.setStrokeWidth(thumbWidth); mInset = thumbWidth / 2 + maskThickness; @@ -175,7 +179,6 @@ public class RoundScrollbarRenderer { } } - /** Returns true if horizontal bounds are updated */ private void updateBounds(Rect bounds) { mRect.set( bounds.left + mInset, @@ -184,6 +187,8 @@ public class RoundScrollbarRenderer { bounds.bottom - mInset); mThumbStrokeWidthAsDegrees = getVertexAngle((mRect.right - mRect.left) / 2f, mThumbPaint.getStrokeWidth() / 2f); + mGapBetweenTrackAndThumbAsDegrees = + getVertexAngle((mRect.right - mRect.left) / 2f, mGapBetweenThumbAndTrackPx); } private float computeSweepAngle(float scrollExtent, float maxScroll) { @@ -262,20 +267,22 @@ public class RoundScrollbarRenderer { // The highest point of the top track on a vertical scale. Here the thumb width is // reduced to account for the arc formed by ROUND stroke style -SCROLLBAR_ANGLE_RANGE / 2f - mThumbStrokeWidthAsDegrees, - // The lowest point of the top track on a vertical scale. Here the thumb width is - // reduced twice to (a) account for the arc formed by ROUND stroke style (b) gap - // between thumb and top track - thumbStartAngle - mThumbStrokeWidthAsDegrees * 2, + // The lowest point of the top track on a vertical scale. It's reduced by + // (a) angular distance for the arc formed by ROUND stroke style + // (b) gap between thumb and top track + thumbStartAngle - mThumbStrokeWidthAsDegrees - mGapBetweenTrackAndThumbAsDegrees, alpha); // Draws the thumb drawArc(canvas, thumbStartAngle, thumbSweepAngle, mThumbPaint); // Draws the bottom arc drawTrack( canvas, - // The highest point of the bottom track on a vertical scale. Here the thumb width - // is added twice to (a) account for the arc formed by ROUND stroke style (b) gap - // between thumb and bottom track - (thumbStartAngle + thumbSweepAngle) + mThumbStrokeWidthAsDegrees * 2, + // The highest point of the bottom track on a vertical scale. Following added to it + // (a) angular distance for the arc formed by ROUND stroke style + // (b) gap between thumb and top track + (thumbStartAngle + thumbSweepAngle) + + mThumbStrokeWidthAsDegrees + + mGapBetweenTrackAndThumbAsDegrees, // The lowest point of the top track on a vertical scale. Here the thumb width is // added to account for the arc formed by ROUND stroke style SCROLLBAR_ANGLE_RANGE / 2f + mThumbStrokeWidthAsDegrees, diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 900f22d2b37b..0d6f82773622 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -133,6 +133,7 @@ import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_ import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; import static com.android.text.flags.Flags.disableHandwritingInitiatorForIme; import static com.android.window.flags.Flags.enableBufferTransformHintFromDisplay; +import static com.android.window.flags.Flags.enableWindowContextResourcesUpdateOnConfigChange; import static com.android.window.flags.Flags.predictiveBackSwipeEdgeNoneApi; import static com.android.window.flags.Flags.setScPropertiesInClient; @@ -271,7 +272,9 @@ import android.window.OnBackInvokedCallback; import android.window.OnBackInvokedDispatcher; import android.window.ScreenCapture; import android.window.SurfaceSyncGroup; +import android.window.WindowContext; import android.window.WindowOnBackInvokedDispatcher; +import android.window.WindowTokenClient; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; @@ -6609,12 +6612,26 @@ public final class ViewRootImpl implements ViewParent, mActivityConfigCallback.onConfigurationChanged(overrideConfig, newDisplayId, activityWindowInfo); } else { - // There is no activity callback - update the configuration right away. + if (enableWindowContextResourcesUpdateOnConfigChange()) { + // There is no activity callback - update resources for window token, if needed. + final WindowTokenClient windowTokenClient = getWindowTokenClient(); + if (windowTokenClient != null) { + windowTokenClient.onConfigurationChanged( + mLastReportedMergedConfiguration.getMergedConfiguration(), + newDisplayId == INVALID_DISPLAY ? mDisplay.getDisplayId() + : newDisplayId); + } + } updateConfiguration(newDisplayId); } mForceNextConfigUpdate = false; } + private WindowTokenClient getWindowTokenClient() { + if (!(mContext instanceof WindowContext)) return null; + return (WindowTokenClient) mContext.getWindowContextToken(); + } + /** * Update display and views if last applied merged configuration changed. * @param newDisplayId Id of new display if moved, {@link Display#INVALID_DISPLAY} otherwise. diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 101d5c950b71..edfa1d5aea1f 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -625,6 +625,12 @@ public interface WindowManager extends ViewManager { int TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH = (1 << 14); // 0x4000 /** + * Transition flag: Indicates that aod is showing hidden by entering doze + * @hide + */ + int TRANSIT_FLAG_AOD_APPEARING = (1 << 15); // 0x8000 + + /** * @hide */ @IntDef(flag = true, prefix = { "TRANSIT_FLAG_" }, value = { @@ -643,6 +649,7 @@ public interface WindowManager extends ViewManager { TRANSIT_FLAG_KEYGUARD_OCCLUDING, TRANSIT_FLAG_KEYGUARD_UNOCCLUDING, TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH, + TRANSIT_FLAG_AOD_APPEARING, }) @Retention(RetentionPolicy.SOURCE) @interface TransitionFlags {} @@ -659,7 +666,8 @@ public interface WindowManager extends ViewManager { (TRANSIT_FLAG_KEYGUARD_GOING_AWAY | TRANSIT_FLAG_KEYGUARD_APPEARING | TRANSIT_FLAG_KEYGUARD_OCCLUDING - | TRANSIT_FLAG_KEYGUARD_UNOCCLUDING); + | TRANSIT_FLAG_KEYGUARD_UNOCCLUDING + | TRANSIT_FLAG_AOD_APPEARING); /** * Remove content mode: Indicates remove content mode is currently not defined. diff --git a/core/java/android/view/accessibility/OWNERS b/core/java/android/view/accessibility/OWNERS index f62b33f1f753..799ef0091f71 100644 --- a/core/java/android/view/accessibility/OWNERS +++ b/core/java/android/view/accessibility/OWNERS @@ -1,4 +1,7 @@ -# Bug component: 44215 +# Bug component: 1530954 +# +# The above component is for automated test bugs. If you are a human looking to report +# a bug in this codebase then please use component 44215. # Android Accessibility Framework owners include /services/accessibility/OWNERS diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig index 37f393ec6511..49a11cab1de9 100644 --- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig +++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig @@ -28,6 +28,14 @@ flag { } flag { + name: "a11y_is_visited_api" + namespace: "accessibility" + description: "Adds an API to indicate whether a URL has been visited or not." + bug: "391469786" + is_exported: true +} + +flag { name: "a11y_overlay_callbacks" is_exported: true namespace: "accessibility" diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 0fb80422833c..56f0415b40cc 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -3778,8 +3778,32 @@ public final class InputMethodManager { ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED); if (Flags.refactorInsetsController()) { - mCurRootView.getInsetsController().hide(WindowInsets.Type.ime(), - false /* fromIme */, statsToken); + synchronized (mH) { + Handler vh = rootView.getHandler(); + if (vh == null) { + // If the view doesn't have a handler, something has changed out from + // under us. + ImeTracker.forLogging().onFailed(statsToken, + ImeTracker.PHASE_CLIENT_VIEW_HANDLER_AVAILABLE); + return; + } + ImeTracker.forLogging().onProgress(statsToken, + ImeTracker.PHASE_CLIENT_VIEW_HANDLER_AVAILABLE); + + if (vh.getLooper() != Looper.myLooper()) { + // The view is running on a different thread than our own, so + // we need to reschedule our work for over there. + if (DEBUG) { + Log.v(TAG, "Close current input: reschedule hide to view thread"); + } + final var viewRootImpl = mCurRootView; + vh.post(() -> viewRootImpl.getInsetsController().hide( + WindowInsets.Type.ime(), false /* fromIme */, statsToken)); + } else { + mCurRootView.getInsetsController().hide(WindowInsets.Type.ime(), + false /* fromIme */, statsToken); + } + } } else { IInputMethodManagerGlobalInvoker.hideSoftInput( mClient, diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java index 785246074cee..1ce5df7cd137 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -55,6 +55,7 @@ public enum DesktopModeFlags { Flags::enableDesktopAppLaunchAlttabTransitionsBugfix, true), ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX(Flags::enableDesktopAppLaunchTransitionsBugfix, true), + ENABLE_DESKTOP_CLOSE_SHORTCUT_BUGFIX(Flags::enableDesktopCloseShortcutBugfix, false), ENABLE_DESKTOP_COMPAT_UI_VISIBILITY_STATUS(Flags::enableCompatUiVisibilityStatus, true), ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX( Flags::enableDesktopRecentsTransitionsCornersBugfix, false), diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index ddbf9e49bb8d..cf21e50e0a19 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -29,6 +29,7 @@ import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; import static android.view.WindowManager.TRANSIT_NONE; @@ -405,7 +406,8 @@ public final class TransitionInfo implements Parcelable { */ public boolean hasChangesOrSideEffects() { return !mChanges.isEmpty() || isKeyguardGoingAway() - || (mFlags & TRANSIT_FLAG_KEYGUARD_APPEARING) != 0; + || (mFlags & TRANSIT_FLAG_KEYGUARD_APPEARING) != 0 + || (mFlags & TRANSIT_FLAG_AOD_APPEARING) != 0; } /** @@ -1289,12 +1291,13 @@ public final class TransitionInfo implements Parcelable { return options; } - /** Make options for a scale-up animation. */ + /** Make options for a scale-up animation with task override option */ @NonNull public static AnimationOptions makeScaleUpAnimOptions(int startX, int startY, int width, - int height) { + int height, boolean overrideTaskTransition) { AnimationOptions options = new AnimationOptions(ANIM_SCALE_UP); options.mTransitionBounds.set(startX, startY, startX + width, startY + height); + options.mOverrideTaskTransition = overrideTaskTransition; return options; } diff --git a/core/java/android/window/WindowTokenClient.java b/core/java/android/window/WindowTokenClient.java index a551fe701c5b..f7bee619bc4b 100644 --- a/core/java/android/window/WindowTokenClient.java +++ b/core/java/android/window/WindowTokenClient.java @@ -106,7 +106,6 @@ public class WindowTokenClient extends Binder { * @param newConfig the updated {@link Configuration} * @param newDisplayId the updated {@link android.view.Display} ID */ - @VisibleForTesting(visibility = PACKAGE) @MainThread public void onConfigurationChanged(Configuration newConfig, int newDisplayId) { onConfigurationChanged(newConfig, newDisplayId, true /* shouldReportConfigChange */); diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index d413ba0b042c..b805ac560b8d 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -572,6 +572,13 @@ flag { } flag { + name: "enable_display_reconnect_interaction" + namespace: "lse_desktop_experience" + description: "Enables new interaction that occurs when a display is reconnected." + bug: "365873835" +} + +flag { name: "show_desktop_experience_dev_option" namespace: "lse_desktop_experience" description: "Replace the freeform windowing dev options with a desktop experience one." @@ -670,6 +677,17 @@ flag { } flag { + name: "enable_window_context_resources_update_on_config_change" + namespace: "lse_desktop_experience" + description: "Updates window context resources before the view receives the config change callback." + bug: "394527409" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "enable_desktop_tab_tearing_minimize_animation_bugfix" namespace: "lse_desktop_experience" description: "Enabling a minimize animation when a new window is opened via tab tearing and the Desktop Windowing open windows limit is reached." @@ -677,4 +695,14 @@ flag { metadata { purpose: PURPOSE_BUGFIX } -}
\ No newline at end of file +} + +flag { + name: "enable_desktop_close_shortcut_bugfix" + namespace: "lse_desktop_experience" + description: "Fix the window-close keyboard shortcut in Desktop Mode." + bug: "394599430" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/com/android/internal/accessibility/OWNERS b/core/java/com/android/internal/accessibility/OWNERS index 1265dfa2c441..dac64f47ba7e 100644 --- a/core/java/com/android/internal/accessibility/OWNERS +++ b/core/java/com/android/internal/accessibility/OWNERS @@ -1,4 +1,7 @@ -# Bug component: 44215 +# Bug component: 1530954 +# +# The above component is for automated test bugs. If you are a human looking to report +# a bug in this codebase then please use component 44215. # Android Accessibility Framework owners include /services/accessibility/OWNERS
\ No newline at end of file diff --git a/core/java/com/android/internal/app/IBatteryStats.aidl b/core/java/com/android/internal/app/IBatteryStats.aidl index a5e166b95177..5f1a641945e8 100644 --- a/core/java/com/android/internal/app/IBatteryStats.aidl +++ b/core/java/com/android/internal/app/IBatteryStats.aidl @@ -35,8 +35,20 @@ import android.telephony.SignalStrength; interface IBatteryStats { /** @hide */ + const int RESULT_OK = 0; + + /** @hide */ + const int RESULT_RUNTIME_EXCEPTION = 1; + + /** @hide */ + const int RESULT_SECURITY_EXCEPTION = 2; + + /** @hide */ const String KEY_UID_SNAPSHOTS = "uid_snapshots"; + /** @hide */ + const String KEY_EXCEPTION_MESSAGE = "exception"; + // These first methods are also called by native code, so must // be kept in sync with frameworks/native/libs/binder/include_batterystats/batterystats/IBatteryStats.h @EnforcePermission("UPDATE_DEVICE_STATS") diff --git a/core/java/com/android/internal/graphics/palette/OWNERS b/core/java/com/android/internal/graphics/palette/OWNERS index 731dca9b128f..df867252c01c 100644 --- a/core/java/com/android/internal/graphics/palette/OWNERS +++ b/core/java/com/android/internal/graphics/palette/OWNERS @@ -1,3 +1,2 @@ -# Bug component: 484670
-dupin@google.com
-jamesoleary@google.com
\ No newline at end of file +# Bug component: 484670 +dupin@google.com diff --git a/core/java/com/android/internal/jank/Cuj.java b/core/java/com/android/internal/jank/Cuj.java index 7c5335cc753c..9085bbec949f 100644 --- a/core/java/com/android/internal/jank/Cuj.java +++ b/core/java/com/android/internal/jank/Cuj.java @@ -306,8 +306,15 @@ public class Cuj { /** Track work utility view animation shrinking when scrolling down app list. */ public static final int CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK = 127; + /** + * Track task transitions + * + * <p>Tracking starts and ends with the animation.</p> + */ + public static final int CUJ_DEFAULT_TASK_TO_TASK_ANIMATION = 128; + // When adding a CUJ, update this and make sure to also update CUJ_TO_STATSD_INTERACTION_TYPE. - @VisibleForTesting static final int LAST_CUJ = CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK; + @VisibleForTesting static final int LAST_CUJ = CUJ_DEFAULT_TASK_TO_TASK_ANIMATION; /** @hide */ @IntDef({ @@ -426,7 +433,8 @@ public class Cuj { CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_ICON, CUJ_DESKTOP_MODE_KEYBOARD_QUICK_SWITCH_APP_LAUNCH, CUJ_LAUNCHER_WORK_UTILITY_VIEW_EXPAND, - CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK + CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK, + CUJ_DEFAULT_TASK_TO_TASK_ANIMATION }) @Retention(RetentionPolicy.SOURCE) public @interface CujType {} @@ -556,6 +564,7 @@ public class Cuj { CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_KEYBOARD_QUICK_SWITCH_APP_LAUNCH] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_KEYBOARD_QUICK_SWITCH_APP_LAUNCH; CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_WORK_UTILITY_VIEW_EXPAND] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_WORK_UTILITY_VIEW_EXPAND; CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_WORK_UTILITY_VIEW_SHRINK; + CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DEFAULT_TASK_TO_TASK_ANIMATION] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DEFAULT_TASK_TO_TASK_ANIMATION; } private Cuj() { @@ -806,6 +815,8 @@ public class Cuj { return "LAUNCHER_WORK_UTILITY_VIEW_EXPAND"; case CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK: return "LAUNCHER_WORK_UTILITY_VIEW_SHRINK"; + case CUJ_DEFAULT_TASK_TO_TASK_ANIMATION: + return "CUJ_DEFAULT_TASK_TO_TASK_ANIMATION"; } return "UNKNOWN"; } diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java index 270cf085b06f..e20a52b24485 100644 --- a/core/java/com/android/internal/policy/DecorView.java +++ b/core/java/com/android/internal/policy/DecorView.java @@ -231,6 +231,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind private int mLastRightInset = 0; @UnsupportedAppUsage private int mLastLeftInset = 0; + private WindowInsets mLastInsets = null; private boolean mLastHasTopStableInset = false; private boolean mLastHasBottomStableInset = false; private boolean mLastHasRightStableInset = false; @@ -1100,6 +1101,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind mLastWindowFlags = attrs.flags; if (insets != null) { + mLastInsets = insets; mLastForceConsumingTypes = insets.getForceConsumingTypes(); mLastForceConsumingOpaqueCaptionBar = insets.isForceConsumingOpaqueCaptionBar(); @@ -1176,6 +1178,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind mForceWindowDrawsBarBackgrounds, requestedVisibleTypes); } + int consumingTypes = 0; // When we expand the window with FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS or // mForceWindowDrawsBarBackgrounds, we still need to ensure that the rest of the view // hierarchy doesn't notice it, unless they've explicitly asked for it. @@ -1186,43 +1189,47 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind // // Note: Once the app uses the R+ Window.setDecorFitsSystemWindows(false) API we no longer // consume insets because they might no longer set SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION. - boolean hideNavigation = (sysUiVisibility & SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0 + final boolean hideNavigation = (sysUiVisibility & SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0 || (requestedVisibleTypes & WindowInsets.Type.navigationBars()) == 0; - boolean decorFitsSystemWindows = mWindow.mDecorFitsSystemWindows; - boolean forceConsumingNavBar = + final boolean decorFitsSystemWindows = mWindow.mDecorFitsSystemWindows; + + final boolean fitsNavBar = + (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0 + && decorFitsSystemWindows + && !hideNavigation; + final boolean forceConsumingNavBar = ((mForceWindowDrawsBarBackgrounds || mDrawLegacyNavigationBarBackgroundHandled) && (attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) == 0 - && (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0 - && decorFitsSystemWindows - && !hideNavigation) + && fitsNavBar) || ((mLastForceConsumingTypes & WindowInsets.Type.navigationBars()) != 0 && hideNavigation); - - boolean consumingNavBar = - ((attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0 - && (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0 - && decorFitsSystemWindows - && !hideNavigation) + final boolean consumingNavBar = + ((attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0 && fitsNavBar) || forceConsumingNavBar; + if (consumingNavBar) { + consumingTypes |= WindowInsets.Type.navigationBars(); + } - // If we didn't request fullscreen layout, but we still got it because of the - // mForceWindowDrawsBarBackgrounds flag, also consume top inset. + // If the fullscreen layout was not requested, but still received because of the + // mForceWindowDrawsBarBackgrounds flag, also consume status bar. // If we should always consume system bars, only consume that if the app wanted to go to // fullscreen, as otherwise we can expect the app to handle it. - boolean fullscreen = (sysUiVisibility & SYSTEM_UI_FLAG_FULLSCREEN) != 0 + final boolean fullscreen = (sysUiVisibility & SYSTEM_UI_FLAG_FULLSCREEN) != 0 || (attrs.flags & FLAG_FULLSCREEN) != 0; final boolean hideStatusBar = fullscreen || (requestedVisibleTypes & WindowInsets.Type.statusBars()) == 0; - boolean consumingStatusBar = - ((sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) == 0 - && decorFitsSystemWindows - && (attrs.flags & FLAG_LAYOUT_IN_SCREEN) == 0 - && (attrs.flags & FLAG_LAYOUT_INSET_DECOR) == 0 - && mForceWindowDrawsBarBackgrounds - && mLastTopInset != 0) + if (((sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) == 0 + && decorFitsSystemWindows + && (attrs.flags & FLAG_LAYOUT_IN_SCREEN) == 0 + && (attrs.flags & FLAG_LAYOUT_INSET_DECOR) == 0 + && mForceWindowDrawsBarBackgrounds + && mLastTopInset != 0) || ((mLastForceConsumingTypes & WindowInsets.Type.statusBars()) != 0 - && hideStatusBar); + && hideStatusBar)) { + consumingTypes |= WindowInsets.Type.statusBars(); + } + // Decide if caption bar need to be consumed final boolean hideCaptionBar = fullscreen || (requestedVisibleTypes & WindowInsets.Type.captionBar()) == 0; final boolean consumingCaptionBar = @@ -1237,22 +1244,23 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind && mLastForceConsumingOpaqueCaptionBar && isOpaqueCaptionBar; - final int consumedTop = - (consumingStatusBar || consumingCaptionBar || consumingOpaqueCaptionBar) - ? mLastTopInset : 0; - int consumedRight = consumingNavBar ? mLastRightInset : 0; - int consumedBottom = consumingNavBar ? mLastBottomInset : 0; - int consumedLeft = consumingNavBar ? mLastLeftInset : 0; + if (consumingCaptionBar || consumingOpaqueCaptionBar) { + consumingTypes |= WindowInsets.Type.captionBar(); + } + + final Insets consumedInsets = mLastInsets != null + ? mLastInsets.getInsets(consumingTypes) : Insets.NONE; if (mContentRoot != null && mContentRoot.getLayoutParams() instanceof MarginLayoutParams) { MarginLayoutParams lp = (MarginLayoutParams) mContentRoot.getLayoutParams(); - if (lp.topMargin != consumedTop || lp.rightMargin != consumedRight - || lp.bottomMargin != consumedBottom || lp.leftMargin != consumedLeft) { - lp.topMargin = consumedTop; - lp.rightMargin = consumedRight; - lp.bottomMargin = consumedBottom; - lp.leftMargin = consumedLeft; + if (lp.topMargin != consumedInsets.top || lp.rightMargin != consumedInsets.right + || lp.bottomMargin != consumedInsets.bottom || lp.leftMargin != + consumedInsets.left) { + lp.topMargin = consumedInsets.top; + lp.rightMargin = consumedInsets.right; + lp.bottomMargin = consumedInsets.bottom; + lp.leftMargin = consumedInsets.left; mContentRoot.setLayoutParams(lp); if (insets == null) { @@ -1261,11 +1269,8 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind requestApplyInsets(); } } - if (insets != null && (consumedLeft > 0 - || consumedTop > 0 - || consumedRight > 0 - || consumedBottom > 0)) { - insets = insets.inset(consumedLeft, consumedTop, consumedRight, consumedBottom); + if (insets != null && !Insets.NONE.equals(consumedInsets)) { + insets = insets.inset(consumedInsets); } } diff --git a/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java b/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java index b7e68bacd143..260619ec0b23 100644 --- a/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java +++ b/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java @@ -23,6 +23,7 @@ import android.content.Context; import android.content.res.Resources; import android.database.ContentObserver; import android.os.Handler; +import android.os.SystemProperties; import android.os.UserHandle; import android.provider.DeviceConfig; import android.provider.Settings; @@ -160,8 +161,13 @@ public class GestureNavigationSettingsObserver extends ContentObserver { } public boolean areNavigationButtonForcedVisible() { - return Settings.Secure.getIntForUser(mContext.getContentResolver(), - Settings.Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_CURRENT) == 0; + String SUWTheme = SystemProperties.get("setupwizard.theme", ""); + boolean isExpressiveThemeEnabled = SUWTheme.equals("glif_expressive") + || SUWTheme.equals("glif_expressive_light"); + // The back gesture is enabled if using the expressive theme + return !isExpressiveThemeEnabled + && Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_CURRENT) == 0; } private float getUnscaledInset(Resources userRes) { diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java index 05a33fe830e8..d8cf258e23ba 100644 --- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java @@ -160,19 +160,21 @@ public abstract class PerfettoProtoLogImpl extends IProtoLogClient.Stub implemen Objects.requireNonNull(mConfigurationService, "A null ProtoLog Configuration Service was provided!"); - try { - var args = createConfigurationServiceRegisterClientArgs(); + mBackgroundLoggingService.execute(() -> { + try { + var args = createConfigurationServiceRegisterClientArgs(); - final var groupArgs = mLogGroups.values().stream() - .map(group -> new RegisterClientArgs - .GroupConfig(group.name(), group.isLogToLogcat())) - .toArray(RegisterClientArgs.GroupConfig[]::new); - args.setGroups(groupArgs); + final var groupArgs = mLogGroups.values().stream() + .map(group -> new RegisterClientArgs + .GroupConfig(group.name(), group.isLogToLogcat())) + .toArray(RegisterClientArgs.GroupConfig[]::new); + args.setGroups(groupArgs); - mConfigurationService.registerClient(this, args); - } catch (RemoteException e) { - throw new RuntimeException("Failed to register ProtoLog client"); - } + mConfigurationService.registerClient(this, args); + } catch (RemoteException e) { + throw new RuntimeException("Failed to register ProtoLog client"); + } + }); } /** diff --git a/core/java/com/android/internal/protolog/WmProtoLogGroups.java b/core/java/com/android/internal/protolog/WmProtoLogGroups.java index 4bd5d24b71e2..5edc2fbd4c8f 100644 --- a/core/java/com/android/internal/protolog/WmProtoLogGroups.java +++ b/core/java/com/android/internal/protolog/WmProtoLogGroups.java @@ -100,6 +100,8 @@ public enum WmProtoLogGroups implements IProtoLogGroup { WM_DEBUG_TPL(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM), WM_DEBUG_EMBEDDED_WINDOWS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM), + WM_DEBUG_PRESENTATION(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, + Consts.TAG_WM), TEST_GROUP(true, true, false, "WindowManagerProtoLogTest"); private final boolean mEnabled; diff --git a/core/java/com/android/internal/widget/NotificationProgressBar.java b/core/java/com/android/internal/widget/NotificationProgressBar.java index 5e82772730b7..905d4dd547f3 100644 --- a/core/java/com/android/internal/widget/NotificationProgressBar.java +++ b/core/java/com/android/internal/widget/NotificationProgressBar.java @@ -83,7 +83,7 @@ public final class NotificationProgressBar extends ProgressBar implements /** @see R.styleable#NotificationProgressBar_trackerHeight */ private final int mTrackerHeight; - private int mTrackerWidth; + private int mTrackerDrawWidth = 0; private int mTrackerPos; private final Matrix mMatrix = new Matrix(); private Matrix mTrackerDrawMatrix = null; @@ -157,7 +157,7 @@ public final class NotificationProgressBar extends ProgressBar implements } else { // TODO: b/372908709 - maybe don't rerun the entire calculation every time the // progress model is updated? For example, if the segments and parts aren't changed, - // there is no need to call `processAndConvertToViewParts` again. + // there is no need to call `processModelAndConvertToViewParts` again. final int progress = mProgressModel.getProgress(); final int progressMax = mProgressModel.getProgressMax(); @@ -286,8 +286,11 @@ public final class NotificationProgressBar extends ProgressBar implements private void configureTrackerBounds() { // Reset the tracker draw matrix to null mTrackerDrawMatrix = null; + mTrackerDrawWidth = 0; - if (mTracker == null || mTrackerHeight <= 0) { + if (mTracker == null) return; + if (mTrackerHeight <= 0) { + mTrackerDrawWidth = mTracker.getIntrinsicWidth(); return; } @@ -306,14 +309,14 @@ public final class NotificationProgressBar extends ProgressBar implements if (dWidth > maxDWidth) { scale = (float) mTrackerHeight / (float) dHeight; dx = (maxDWidth * scale - dWidth * scale) * 0.5f; - mTrackerWidth = (int) (maxDWidth * scale); + mTrackerDrawWidth = (int) (maxDWidth * scale); } else if (dHeight > maxDHeight) { scale = (float) mTrackerHeight * 0.5f / (float) dWidth; dy = (maxDHeight * scale - dHeight * scale) * 0.5f; - mTrackerWidth = mTrackerHeight / 2; + mTrackerDrawWidth = mTrackerHeight / 2; } else { scale = (float) mTrackerHeight / (float) dHeight; - mTrackerWidth = (int) (dWidth * scale); + mTrackerDrawWidth = (int) (dWidth * scale); } mTrackerDrawMatrix.setScale(scale, scale); @@ -449,7 +452,8 @@ public final class NotificationProgressBar extends ProgressBar implements segSegGap, segPointGap, pointRadius, - mHasTrackerIcon + mHasTrackerIcon, + mTrackerDrawWidth ); final float segmentMinWidth = mNotificationProgressDrawable.getSegmentMinWidth(); @@ -465,7 +469,6 @@ public final class NotificationProgressBar extends ProgressBar implements segmentMinWidth, pointRadius, progressFraction, - width, isStyledByProgress, progressGap ); @@ -493,8 +496,8 @@ public final class NotificationProgressBar extends ProgressBar implements pointRadius, mHasTrackerIcon, segmentMinWidth, - isStyledByProgress - ); + isStyledByProgress, + mTrackerDrawWidth); } catch (NotEnoughWidthToFitAllPartsException ex) { Log.w(TAG, "Failed to stretch and rescale segments with single segment fallback", ex); @@ -522,8 +525,8 @@ public final class NotificationProgressBar extends ProgressBar implements pointRadius, mHasTrackerIcon, segmentMinWidth, - isStyledByProgress - ); + isStyledByProgress, + mTrackerDrawWidth); } catch (NotEnoughWidthToFitAllPartsException ex) { Log.w(TAG, "Failed to stretch and rescale segments with single segments and no points", @@ -537,16 +540,20 @@ public final class NotificationProgressBar extends ProgressBar implements mParts, mProgressDrawableParts, progressFraction, - width, isStyledByProgress, progressGap); } + // Extend the first and last segments to fill the entire width. + p.first.getFirst().setStart(0); + p.first.getLast().setEnd(width); + if (DEBUG) { Log.d(TAG, "Updating NotificationProgressDrawable parts"); } mNotificationProgressDrawable.setParts(p.first); - mAdjustedProgressFraction = p.second / width; + mAdjustedProgressFraction = + (p.second - mTrackerDrawWidth / 2F) / (width - mTrackerDrawWidth); } private void updateTrackerAndBarPos(int w, int h) { @@ -607,7 +614,7 @@ public final class NotificationProgressBar extends ProgressBar implements int available = w - mPaddingLeft - mPaddingRight; final int trackerWidth = tracker.getIntrinsicWidth(); final int trackerHeight = tracker.getIntrinsicHeight(); - available -= ((mTrackerHeight <= 0) ? trackerWidth : mTrackerWidth); + available -= mTrackerDrawWidth; final int trackerPos = (int) (progressFraction * available + 0.5f); @@ -672,7 +679,7 @@ public final class NotificationProgressBar extends ProgressBar implements canvas.translate(mPaddingLeft + mTrackerPos, mPaddingTop); if (mTrackerHeight > 0) { - canvas.clipRect(0, 0, mTrackerWidth, mTrackerHeight); + canvas.clipRect(0, 0, mTrackerDrawWidth, mTrackerHeight); } if (mTrackerDrawMatrix != null) { @@ -751,6 +758,7 @@ public final class NotificationProgressBar extends ProgressBar implements throw new IllegalArgumentException("Invalid progress : " + progress); } + for (ProgressStyle.Point point : points) { final int pos = point.getPosition(); if (pos < 0 || pos > progressMax) { @@ -758,6 +766,19 @@ public final class NotificationProgressBar extends ProgressBar implements } } + // There should be no points at start or end. If there are, drop them with a warning. + points.removeIf(point -> { + final int pos = point.getPosition(); + if (pos == 0) { + Log.w(TAG, "Dropping point at start"); + return true; + } else if (pos == progressMax) { + Log.w(TAG, "Dropping point at end"); + return true; + } + return false; + }); + final Map<Integer, ProgressStyle.Segment> startToSegmentMap = generateStartToSegmentMap( segments); final Map<Integer, ProgressStyle.Point> positionToPointMap = generatePositionToPointMap( @@ -891,12 +912,14 @@ public final class NotificationProgressBar extends ProgressBar implements float segSegGap, float segPointGap, float pointRadius, - boolean hasTrackerIcon - ) { + boolean hasTrackerIcon, + int trackerDrawWidth) { List<DrawablePart> drawableParts = new ArrayList<>(); - // generally, we will start drawing at (x, y) and end at (x+w, y) - float x = (float) 0; + float available = totalWidth - trackerDrawWidth; + // Generally, we will start the first segment at (x+trackerDrawWidth/2, y) and end the last + // segment at (x+w-trackerDrawWidth/2, y) + float x = trackerDrawWidth / 2F; final int nParts = parts.size(); for (int iPart = 0; iPart < nParts; iPart++) { @@ -904,15 +927,14 @@ public final class NotificationProgressBar extends ProgressBar implements final Part prevPart = iPart == 0 ? null : parts.get(iPart - 1); final Part nextPart = iPart + 1 == nParts ? null : parts.get(iPart + 1); if (part instanceof Segment segment) { - final float segWidth = segment.mFraction * totalWidth; + final float segWidth = segment.mFraction * available; // Advance the start position to account for a point immediately prior. - final float startOffset = getSegStartOffset(prevPart, pointRadius, segPointGap, - iPart == 1); + final float startOffset = getSegStartOffset(prevPart, pointRadius, segPointGap); final float start = x + startOffset; // Retract the end position to account for the padding and a point immediately // after. final float endOffset = getSegEndOffset(segment, nextPart, pointRadius, segPointGap, - segSegGap, iPart == nParts - 2, hasTrackerIcon); + segSegGap, hasTrackerIcon); final float end = x + segWidth - endOffset; drawableParts.add(new DrawableSegment(start, end, segment.mColor, segment.mFaded)); @@ -927,16 +949,6 @@ public final class NotificationProgressBar extends ProgressBar implements final float pointWidth = 2 * pointRadius; float start = x - pointRadius; float end = x + pointRadius; - // Only shift the points right at the start/end. - // For the points close to the start/end, the segment minimum width requirement - // would take care of shifting them to be within the bounds. - if (iPart == 0) { - start = 0; - end = pointWidth; - } else if (iPart == nParts - 1) { - start = totalWidth - pointWidth; - end = totalWidth; - } drawableParts.add(new DrawablePoint(start, end, point.mColor)); } @@ -945,16 +957,13 @@ public final class NotificationProgressBar extends ProgressBar implements return drawableParts; } - private static float getSegStartOffset(Part prevPart, float pointRadius, float segPointGap, - boolean isSecondPart) { + private static float getSegStartOffset(Part prevPart, float pointRadius, float segPointGap) { if (!(prevPart instanceof Point)) return 0F; - final float pointOffset = isSecondPart ? pointRadius : 0; - return pointOffset + pointRadius + segPointGap; + return pointRadius + segPointGap; } private static float getSegEndOffset(Segment seg, Part nextPart, float pointRadius, - float segPointGap, float segSegGap, boolean isSecondToLastPart, - boolean hasTrackerIcon) { + float segPointGap, float segSegGap, boolean hasTrackerIcon) { if (nextPart == null) return 0F; if (nextPart instanceof Segment nextSeg) { if (!seg.mFaded && nextSeg.mFaded) { @@ -964,8 +973,7 @@ public final class NotificationProgressBar extends ProgressBar implements return segSegGap; } - final float pointOffset = isSecondToLastPart ? pointRadius : 0; - return segPointGap + pointRadius + pointOffset; + return segPointGap + pointRadius; } /** @@ -980,7 +988,6 @@ public final class NotificationProgressBar extends ProgressBar implements float segmentMinWidth, float pointRadius, float progressFraction, - float totalWidth, boolean isStyledByProgress, float progressGap ) throws NotEnoughWidthToFitAllPartsException { @@ -1003,7 +1010,6 @@ public final class NotificationProgressBar extends ProgressBar implements parts, drawableParts, progressFraction, - totalWidth, isStyledByProgress, progressGap); } @@ -1056,7 +1062,6 @@ public final class NotificationProgressBar extends ProgressBar implements parts, drawableParts, progressFraction, - totalWidth, isStyledByProgress, progressGap); } @@ -1071,11 +1076,12 @@ public final class NotificationProgressBar extends ProgressBar implements List<Part> parts, List<DrawablePart> drawableParts, float progressFraction, - float totalWidth, boolean isStyledByProgress, float progressGap ) { - if (progressFraction == 1) return new Pair<>(drawableParts, totalWidth); + if (progressFraction == 1) { + return new Pair<>(drawableParts, drawableParts.getLast().getEnd()); + } int iPartFirstSegmentToStyle = -1; int iPartSegmentToSplit = -1; @@ -1162,14 +1168,15 @@ public final class NotificationProgressBar extends ProgressBar implements float pointRadius, boolean hasTrackerIcon, float segmentMinWidth, - boolean isStyledByProgress + boolean isStyledByProgress, + int trackerDrawWidth ) throws NotEnoughWidthToFitAllPartsException { List<Part> parts = processModelAndConvertToViewParts(segments, points, progress, progressMax); List<DrawablePart> drawableParts = processPartsAndConvertToDrawableParts(parts, totalWidth, - segSegGap, segPointGap, pointRadius, hasTrackerIcon); + segSegGap, segPointGap, pointRadius, hasTrackerIcon, trackerDrawWidth); return maybeStretchAndRescaleSegments(parts, drawableParts, segmentMinWidth, pointRadius, - getProgressFraction(progressMax, progress), totalWidth, isStyledByProgress, + getProgressFraction(progressMax, progress), isStyledByProgress, hasTrackerIcon ? 0F : segSegGap); } diff --git a/core/jni/android_view_DisplayEventReceiver.cpp b/core/jni/android_view_DisplayEventReceiver.cpp index d8f1b626abf2..31b9fd1ad170 100644 --- a/core/jni/android_view_DisplayEventReceiver.cpp +++ b/core/jni/android_view_DisplayEventReceiver.cpp @@ -284,6 +284,8 @@ void NativeDisplayEventReceiver::dispatchModeRejected(PhysicalDisplayId displayI displayId.value, modeId); ALOGV("receiver %p ~ Returned from Mode Rejected handler.", this); } + + mMessageQueue->raiseAndClearException(env, "dispatchModeRejected"); } void NativeDisplayEventReceiver::dispatchFrameRateOverrides( @@ -314,7 +316,7 @@ void NativeDisplayEventReceiver::dispatchFrameRateOverrides( ALOGV("receiver %p ~ Returned from FrameRateOverride handler.", this); } - mMessageQueue->raiseAndClearException(env, "dispatchModeChanged"); + mMessageQueue->raiseAndClearException(env, "dispatchFrameRateOverrides"); } void NativeDisplayEventReceiver::dispatchHdcpLevelsChanged(PhysicalDisplayId displayId, diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index c9f4cdc8e3ce..51049889ecd6 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -9385,6 +9385,10 @@ android:permission="android.permission.BIND_JOB_SERVICE"> </service> + <service android:name="com.android.server.security.UpdateCertificateRevocationStatusJobService" + android:permission="android.permission.BIND_JOB_SERVICE"> + </service> + <service android:name="com.android.server.pm.PackageManagerShellCommandDataLoader" android:exported="false"> <intent-filter> diff --git a/core/res/res/values-w225dp/dimens.xml b/core/res/res/values-w225dp/dimens.xml new file mode 100644 index 000000000000..0cd3293f0894 --- /dev/null +++ b/core/res/res/values-w225dp/dimens.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 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 + ~ + ~ https://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. + --> +<resources> + <!-- The width of the round scrollbar --> + <dimen name="round_scrollbar_width">6dp</dimen> +</resources> diff --git a/core/res/res/values-watch/config.xml b/core/res/res/values-watch/config.xml index 4ff3f8825cc4..ef5875eff06f 100644 --- a/core/res/res/values-watch/config.xml +++ b/core/res/res/values-watch/config.xml @@ -110,4 +110,8 @@ tap power gesture from triggering the selected target action. --> <integer name="config_doubleTapPowerGestureMode">0</integer> + + <!-- By default ActivityOptions#makeScaleUpAnimation is only used between activities. This + config enables OEMs to support its usage across tasks.--> + <bool name="config_enableCrossTaskScaleUpAnimation">true</bool> </resources> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 8db94a420e4c..17acf9aed278 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -3262,6 +3262,14 @@ as private. {@see android.view.Display#FLAG_PRIVATE} --> <integer-array translatable="false" name="config_localPrivateDisplayPorts"></integer-array> + <!-- Controls if local secondary displays should be able to steal focus and become top display. + Value specified in the array represents physical port address of each display and displays + in this list due to flag dependencies will be marked with the following flags: + {@see android.view.Display#FLAG_STEAL_TOP_FOCUS_DISABLED} + {@see android.view.Display#FLAG_OWN_FOCUS} --> + <integer-array translatable="false" name="config_localNotStealTopFocusDisplayPorts"> + </integer-array> + <!-- The default mode for the default display. One of the following values (See Display.java): 0 - COLOR_MODE_DEFAULT 7 - COLOR_MODE_SRGB @@ -7248,6 +7256,9 @@ <!-- Wear devices: An intent action that is used for remote intent. --> <string name="config_wearRemoteIntentAction" translatable="false" /> + <!-- Whether the current device's internal display can host desktop sessions. --> + <bool name="config_canInternalDisplayHostDesktops">false</bool> + <!-- Whether desktop mode is supported on the current device --> <bool name="config_isDesktopModeSupported">false</bool> @@ -7340,4 +7351,23 @@ <!-- Array containing the notification assistant service adjustments that are not supported by default on this device--> <string-array translatable="false" name="config_notificationDefaultUnsupportedAdjustments" /> + + <!-- Preference name of bugreport--> + <string name="prefs_bugreport" translatable="false">bugreports</string> + + <!-- key value of warning state stored in bugreport preference--> + <string name="key_warning_state" translatable="false">warning-state</string> + + <!-- Bugreport warning dialog state unknown--> + <integer name="bugreport_state_unknown">0</integer> + + <!-- Bugreport warning dialog state shows the warning dialog--> + <integer name="bugreport_state_show">1</integer> + + <!-- Bugreport warning dialog state skips the warning dialog--> + <integer name="bugreport_state_hide">2</integer> + + <!-- By default ActivityOptions#makeScaleUpAnimation is only used between activities. This + config enables OEMs to support its usage across tasks.--> + <bool name="config_enableCrossTaskScaleUpAnimation">false</bool> </resources> diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 484e8ef1e049..595160ec9f66 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -782,6 +782,9 @@ aliasing effects). This is only used on circular displays. --> <dimen name="circular_display_mask_thickness">1px</dimen> + <!-- The width of the round scrollbar --> + <dimen name="round_scrollbar_width">5dp</dimen> + <dimen name="lock_pattern_dot_line_width">22dp</dimen> <dimen name="lock_pattern_dot_size">14dp</dimen> <dimen name="lock_pattern_dot_size_activated">30dp</dimen> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 8c2ca97af493..cc2897a2779e 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -430,6 +430,7 @@ <java-symbol type="bool" name="config_enableProximityService" /> <java-symbol type="bool" name="config_enableVirtualDeviceManager" /> <java-symbol type="array" name="config_localPrivateDisplayPorts" /> + <java-symbol type="array" name="config_localNotStealTopFocusDisplayPorts" /> <java-symbol type="integer" name="config_defaultDisplayDefaultColorMode" /> <java-symbol type="bool" name="config_enableAppWidgetService" /> <java-symbol type="dimen" name="config_pictureInPictureMinAspectRatio" /> @@ -585,6 +586,7 @@ <java-symbol type="dimen" name="accessibility_magnification_indicator_width" /> <java-symbol type="dimen" name="circular_display_mask_thickness" /> <java-symbol type="dimen" name="user_icon_size" /> + <java-symbol type="dimen" name="round_scrollbar_width" /> <java-symbol type="string" name="add_account_button_label" /> <java-symbol type="string" name="addToDictionary" /> @@ -5752,6 +5754,9 @@ <!-- Whether the developer option for desktop mode is supported on the current device --> <java-symbol type="bool" name="config_isDesktopModeDevOptionSupported" /> + <!-- Whether the current device's internal display can host desktop sessions. --> + <java-symbol type="bool" name="config_canInternalDisplayHostDesktops" /> + <!-- Maximum number of active tasks on a given Desktop Windowing session. Set to 0 for unlimited. --> <java-symbol type="integer" name="config_maxDesktopWindowingActiveTasks"/> @@ -5900,4 +5905,14 @@ <java-symbol type="string" name="usb_apm_usb_plugged_in_when_locked_notification_text" /> <java-symbol type="string" name="usb_apm_usb_suspicious_activity_notification_title" /> <java-symbol type="string" name="usb_apm_usb_suspicious_activity_notification_text" /> + + <java-symbol type="string" name="prefs_bugreport" /> + <java-symbol type="string" name="key_warning_state" /> + <java-symbol type="integer" name="bugreport_state_unknown" /> + <java-symbol type="integer" name="bugreport_state_show" /> + <java-symbol type="integer" name="bugreport_state_hide" /> + + <!-- Enable OEMs to support scale up anim across tasks.--> + <java-symbol type="bool" name="config_enableCrossTaskScaleUpAnimation" /> + </resources> diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java index ca6ad6fae46e..f89e4416ce78 100644 --- a/core/tests/coretests/src/android/app/NotificationTest.java +++ b/core/tests/coretests/src/android/app/NotificationTest.java @@ -2532,6 +2532,46 @@ public class NotificationTest { @Test @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_addProgressPoint_dropsZeroPoints() { + // GIVEN + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + // Points should not be a negative integer. + progressStyle + .addProgressPoint(new Notification.ProgressStyle.Point(0)); + + // THEN + assertThat(progressStyle.getProgressPoints()).isEmpty(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_setProgressPoint_dropsZeroPoints() { + // GIVEN + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + // Points should not be a negative integer. + progressStyle + .setProgressPoints(List.of(new Notification.ProgressStyle.Point(0))); + + // THEN + assertThat(progressStyle.getProgressPoints()).isEmpty(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_createProgressModel_ignoresPointsAtMax() { + // GIVEN + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + progressStyle.addProgressSegment(new Notification.ProgressStyle.Segment(100)); + // Points should not larger than progress maximum. + progressStyle + .addProgressPoint(new Notification.ProgressStyle.Point(100)); + + // THEN + assertThat(progressStyle.createProgressModel(Color.BLUE, Color.RED).getPoints()).isEmpty(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) public void progressStyle_createProgressModel_ignoresPointsExceedingMax() { // GIVEN final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); @@ -2573,14 +2613,14 @@ public class NotificationTest { // THEN assertThat(progressStyle.createProgressModel(defaultProgressColor, backgroundColor) .getPoints()).isEqualTo( - List.of(new Notification.ProgressStyle.Point(0) - .setColor(expectedProgressColor), - new Notification.ProgressStyle.Point(20) + List.of(new Notification.ProgressStyle.Point(20) .setColor(expectedProgressColor), new Notification.ProgressStyle.Point(50) .setColor(expectedProgressColor), new Notification.ProgressStyle.Point(70) - .setColor(expectedProgressColor) + .setColor(expectedProgressColor), + new Notification.ProgressStyle.Point(80) + .setColor(expectedProgressColor) ) ); } diff --git a/core/tests/coretests/src/android/content/IntentTest.java b/core/tests/coretests/src/android/content/IntentTest.java index fdfb0c34cdeb..fa1948d9786c 100644 --- a/core/tests/coretests/src/android/content/IntentTest.java +++ b/core/tests/coretests/src/android/content/IntentTest.java @@ -23,6 +23,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.os.Binder; +import android.os.Bundle; import android.os.IBinder; import android.os.Parcel; import android.platform.test.annotations.Presubmit; @@ -238,4 +239,42 @@ public class IntentTest { // Not all keys from intent are kept - clip data keys are dropped. assertFalse(intent.getExtraIntentKeys().containsAll(originalIntentKeys)); } + + @Test + public void testSetIntentExtrasClassLoaderEffectiveAfterExtraBundleUnparcel() { + Intent intent = new Intent(); + intent.putExtra("bundle", new Bundle()); + + final Parcel parcel = Parcel.obtain(); + intent.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + final Intent target = new Intent(); + target.readFromParcel(parcel); + target.collectExtraIntentKeys(); + ClassLoader cl = new ClassLoader() { + }; + target.setExtrasClassLoader(cl); + assertThat(target.getBundleExtra("bundle").getClassLoader()).isEqualTo(cl); + } + + @Test + public void testBundlePutAllClassLoader() { + Intent intent = new Intent(); + Bundle b1 = new Bundle(); + b1.putBundle("bundle", new Bundle()); + intent.putExtra("b1", b1); + final Parcel parcel = Parcel.obtain(); + intent.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + final Intent target = new Intent(); + target.readFromParcel(parcel); + + ClassLoader cl = new ClassLoader() { + }; + target.setExtrasClassLoader(cl); + Bundle b2 = new Bundle(); + b2.putAll(target.getBundleExtra("b1")); + assertThat(b2.getBundle("bundle").getClassLoader()).isEqualTo(cl); + } + } diff --git a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java index 8fa510381060..dc2f0a69375d 100644 --- a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java +++ b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java @@ -307,8 +307,10 @@ public class DisplayManagerGlobalTest { assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_ADDED, mDisplayManagerGlobal .mapFiltersToInternalEventFlag(DisplayManager.EVENT_TYPE_DISPLAY_ADDED, 0)); - assertEquals(DISPLAY_CHANGE_EVENTS, mDisplayManagerGlobal - .mapFiltersToInternalEventFlag(DisplayManager.EVENT_TYPE_DISPLAY_CHANGED, 0)); + assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED, + mDisplayManagerGlobal + .mapFiltersToInternalEventFlag(DisplayManager.EVENT_TYPE_DISPLAY_CHANGED, + 0)); assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REMOVED, mDisplayManagerGlobal.mapFiltersToInternalEventFlag( DisplayManager.EVENT_TYPE_DISPLAY_REMOVED, 0)); diff --git a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java index 45d66e8ee3a9..e6361e10cfa7 100644 --- a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java +++ b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java @@ -123,15 +123,21 @@ public class InsetsSourceConsumerTest { @Test public void testSetControl_cancelAnimation() { InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { - final InsetsSourceControl newControl = new InsetsSourceControl(mConsumer.getControl()); + final int[] cancelTypes = {0}; - // Change the side of the insets hint. - newControl.setInsetsHint(Insets.of(0, 0, 0, 100)); + // Change the side of the insets hint from NONE to BOTTOM. + final InsetsSourceControl newControl1 = new InsetsSourceControl(mConsumer.getControl()); + newControl1.setInsetsHint(Insets.of(0, 0, 0, 100)); + mConsumer.setControl(newControl1, new int[1], new int[1], cancelTypes, new int[1]); - int[] cancelTypes = {0}; - mConsumer.setControl(newControl, new int[1], new int[1], cancelTypes, new int[1]); + assertEquals("The animation must not be cancelled", 0, cancelTypes[0]); - assertEquals(statusBars(), cancelTypes[0]); + // Change the side of the insets hint from BOTTOM to TOP. + final InsetsSourceControl newControl2 = new InsetsSourceControl(mConsumer.getControl()); + newControl2.setInsetsHint(Insets.of(0, 100, 0, 0)); + mConsumer.setControl(newControl2, new int[1], new int[1], cancelTypes, new int[1]); + + assertEquals("The animation must be cancelled", statusBars(), cancelTypes[0]); }); } diff --git a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java index 9baa31faea08..282886af9ef8 100644 --- a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java +++ b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java @@ -121,18 +121,20 @@ public class NotificationProgressBarTest { assertThat(parts).isEqualTo(expectedParts); - float drawableWidth = 300; + float drawableWidth = 320; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; List<DrawablePart> drawableParts = NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon, + trackerDrawWidth); List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 300, Color.RED))); + List.of(new DrawableSegment(10, 310, Color.RED))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); @@ -141,14 +143,14 @@ public class NotificationProgressBarTest { Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 50% opacity int fadedRed = 0x80FF0000; expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 300, fadedRed, true))); + List.of(new DrawableSegment(10, 310, fadedRed, true))); - assertThat(p.second).isEqualTo(0); + assertThat(p.second).isEqualTo(10); assertThat(p.first).isEqualTo(expectedDrawableParts); } @@ -168,18 +170,20 @@ public class NotificationProgressBarTest { assertThat(parts).isEqualTo(expectedParts); - float drawableWidth = 300; + float drawableWidth = 320; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; List<DrawablePart> drawableParts = NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon, + trackerDrawWidth); List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 300, Color.RED))); + List.of(new DrawableSegment(10, 310, Color.RED))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); @@ -188,9 +192,9 @@ public class NotificationProgressBarTest { Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); - assertThat(p.second).isEqualTo(300); + assertThat(p.second).isEqualTo(310); assertThat(p.first).isEqualTo(expectedDrawableParts); } @@ -219,6 +223,42 @@ public class NotificationProgressBarTest { progressMax); } + @Test + public void processAndConvertToParts_pointPositionIsZero() { + List<ProgressStyle.Segment> segments = new ArrayList<>(); + segments.add(new ProgressStyle.Segment(100).setColor(Color.RED)); + List<ProgressStyle.Point> points = new ArrayList<>(); + points.add(new ProgressStyle.Point(0).setColor(Color.RED)); + int progress = 50; + int progressMax = 100; + + List<Part> parts = NotificationProgressBar.processModelAndConvertToViewParts(segments, + points, progress, progressMax); + + // Point at the start is dropped. + List<Part> expectedParts = new ArrayList<>(List.of(new Segment(1f, Color.RED))); + + assertThat(parts).isEqualTo(expectedParts); + } + + @Test + public void processAndConvertToParts_pointPositionAtMax() { + List<ProgressStyle.Segment> segments = new ArrayList<>(); + segments.add(new ProgressStyle.Segment(100).setColor(Color.RED)); + List<ProgressStyle.Point> points = new ArrayList<>(); + points.add(new ProgressStyle.Point(100).setColor(Color.RED)); + int progress = 50; + int progressMax = 100; + + List<Part> parts = NotificationProgressBar.processModelAndConvertToViewParts(segments, + points, progress, progressMax); + + // Point at the end is dropped. + List<Part> expectedParts = new ArrayList<>(List.of(new Segment(1f, Color.RED))); + + assertThat(parts).isEqualTo(expectedParts); + } + @Test(expected = IllegalArgumentException.class) public void processAndConvertToParts_pointPositionAboveMax() { List<ProgressStyle.Segment> segments = new ArrayList<>(); @@ -249,18 +289,20 @@ public class NotificationProgressBarTest { assertThat(parts).isEqualTo(expectedParts); - float drawableWidth = 300; + float drawableWidth = 320; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; List<DrawablePart> drawableParts = NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon, + trackerDrawWidth); List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 300, Color.BLUE))); + List.of(new DrawableSegment(10, 310, Color.BLUE))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); @@ -269,15 +311,15 @@ public class NotificationProgressBarTest { Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 50% opacity int fadedBlue = 0x800000FF; expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 180, Color.BLUE), - new DrawableSegment(180, 300, fadedBlue, true))); + List.of(new DrawableSegment(10, 190, Color.BLUE), + new DrawableSegment(190, 310, fadedBlue, true))); - assertThat(p.second).isEqualTo(180); + assertThat(p.second).isEqualTo(190); assertThat(p.first).isEqualTo(expectedDrawableParts); } @@ -299,19 +341,21 @@ public class NotificationProgressBarTest { assertThat(parts).isEqualTo(expectedParts); - float drawableWidth = 300; + float drawableWidth = 320; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; List<DrawablePart> drawableParts = NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon, + trackerDrawWidth); List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 146, Color.RED), - new DrawableSegment(150, 300, Color.GREEN))); + List.of(new DrawableSegment(10, 156, Color.RED), + new DrawableSegment(160, 310, Color.GREEN))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); @@ -319,15 +363,15 @@ public class NotificationProgressBarTest { boolean isStyledByProgress = true; Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 50% opacity int fadedGreen = 0x8000FF00; - expectedDrawableParts = new ArrayList<>(List.of(new DrawableSegment(0, 146, Color.RED), - new DrawableSegment(150, 180, Color.GREEN), - new DrawableSegment(180, 300, fadedGreen, true))); + expectedDrawableParts = new ArrayList<>(List.of(new DrawableSegment(10, 156, Color.RED), + new DrawableSegment(160, 190, Color.GREEN), + new DrawableSegment(190, 310, fadedGreen, true))); - assertThat(p.second).isEqualTo(180); + assertThat(p.second).isEqualTo(190); assertThat(p.first).isEqualTo(expectedDrawableParts); } @@ -353,10 +397,12 @@ public class NotificationProgressBarTest { float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = false; + int trackerDrawWidth = 0; List<DrawablePart> drawableParts = NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon, + trackerDrawWidth); List<DrawablePart> expectedDrawableParts = new ArrayList<>( List.of(new DrawableSegment(0, 146, Color.RED), @@ -368,7 +414,7 @@ public class NotificationProgressBarTest { boolean isStyledByProgress = true; Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 50% opacity int fadedGreen = 0x8000FF00; @@ -409,26 +455,28 @@ public class NotificationProgressBarTest { assertThat(parts).isEqualTo(expectedParts); - float drawableWidth = 300; + float drawableWidth = 320; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; List<DrawablePart> drawableParts = NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon, + trackerDrawWidth); List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 35, Color.BLUE), - new DrawablePoint(39, 51, Color.RED), - new DrawableSegment(55, 65, Color.BLUE), - new DrawablePoint(69, 81, Color.BLUE), - new DrawableSegment(85, 170, Color.BLUE), - new DrawablePoint(174, 186, Color.BLUE), - new DrawableSegment(190, 215, Color.BLUE), - new DrawablePoint(219, 231, Color.YELLOW), - new DrawableSegment(235, 300, Color.BLUE))); + List.of(new DrawableSegment(10, 45, Color.BLUE), + new DrawablePoint(49, 61, Color.RED), + new DrawableSegment(65, 75, Color.BLUE), + new DrawablePoint(79, 91, Color.BLUE), + new DrawableSegment(95, 180, Color.BLUE), + new DrawablePoint(184, 196, Color.BLUE), + new DrawableSegment(200, 225, Color.BLUE), + new DrawablePoint(229, 241, Color.YELLOW), + new DrawableSegment(245, 310, Color.BLUE))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); @@ -437,23 +485,23 @@ public class NotificationProgressBarTest { Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 50% opacity int fadedBlue = 0x800000FF; int fadedYellow = 0x80FFFF00; expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 34.219177F, Color.BLUE), - new DrawablePoint(38.219177F, 50.219177F, Color.RED), - new DrawableSegment(54.219177F, 70.21918F, Color.BLUE), - new DrawablePoint(74.21918F, 86.21918F, Color.BLUE), - new DrawableSegment(90.21918F, 172.38356F, Color.BLUE), - new DrawablePoint(176.38356F, 188.38356F, Color.BLUE), - new DrawableSegment(192.38356F, 217.0137F, fadedBlue, true), - new DrawablePoint(221.0137F, 233.0137F, fadedYellow), - new DrawableSegment(237.0137F, 300F, fadedBlue, true))); - - assertThat(p.second).isEqualTo(182.38356F); + List.of(new DrawableSegment(10, 44.219177F, Color.BLUE), + new DrawablePoint(48.219177F, 60.219177F, Color.RED), + new DrawableSegment(64.219177F, 80.21918F, Color.BLUE), + new DrawablePoint(84.21918F, 96.21918F, Color.BLUE), + new DrawableSegment(100.21918F, 182.38356F, Color.BLUE), + new DrawablePoint(186.38356F, 198.38356F, Color.BLUE), + new DrawableSegment(202.38356F, 227.0137F, fadedBlue, true), + new DrawablePoint(231.0137F, 243.0137F, fadedYellow), + new DrawableSegment(247.0137F, 310F, fadedBlue, true))); + + assertThat(p.second).isEqualTo(192.38356F); assertThat(p.first).isEqualTo(expectedDrawableParts); } @@ -488,102 +536,29 @@ public class NotificationProgressBarTest { assertThat(parts).isEqualTo(expectedParts); - float drawableWidth = 300; - float segSegGap = 4; - float segPointGap = 4; - float pointRadius = 6; - boolean hasTrackerIcon = true; - List<DrawablePart> drawableParts = - NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); - - List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 35, Color.RED), new DrawablePoint(39, 51, Color.RED), - new DrawableSegment(55, 65, Color.RED), - new DrawablePoint(69, 81, Color.BLUE), - new DrawableSegment(85, 146, Color.RED), - new DrawableSegment(150, 170, Color.GREEN), - new DrawablePoint(174, 186, Color.BLUE), - new DrawableSegment(190, 215, Color.GREEN), - new DrawablePoint(219, 231, Color.YELLOW), - new DrawableSegment(235, 300, Color.GREEN))); - - assertThat(drawableParts).isEqualTo(expectedDrawableParts); - - float segmentMinWidth = 16; - boolean isStyledByProgress = true; - - Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( - parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); - - // Colors with 50% opacity - int fadedGreen = 0x8000FF00; - int fadedYellow = 0x80FFFF00; - expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 34.095238F, Color.RED), - new DrawablePoint(38.095238F, 50.095238F, Color.RED), - new DrawableSegment(54.095238F, 70.09524F, Color.RED), - new DrawablePoint(74.09524F, 86.09524F, Color.BLUE), - new DrawableSegment(90.09524F, 148.9524F, Color.RED), - new DrawableSegment(152.95238F, 172.7619F, Color.GREEN), - new DrawablePoint(176.7619F, 188.7619F, Color.BLUE), - new DrawableSegment(192.7619F, 217.33333F, fadedGreen, true), - new DrawablePoint(221.33333F, 233.33333F, fadedYellow), - new DrawableSegment(237.33333F, 299.99997F, fadedGreen, true))); - - assertThat(p.second).isEqualTo(182.7619F); - assertThat(p.first).isEqualTo(expectedDrawableParts); - } - - @Test - public void processAndConvertToParts_multipleSegmentsWithPointsAtStartAndEnd() - throws NotEnoughWidthToFitAllPartsException { - List<ProgressStyle.Segment> segments = new ArrayList<>(); - segments.add(new ProgressStyle.Segment(50).setColor(Color.RED)); - segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN)); - List<ProgressStyle.Point> points = new ArrayList<>(); - points.add(new ProgressStyle.Point(0).setColor(Color.RED)); - points.add(new ProgressStyle.Point(25).setColor(Color.BLUE)); - points.add(new ProgressStyle.Point(60).setColor(Color.BLUE)); - points.add(new ProgressStyle.Point(100).setColor(Color.YELLOW)); - int progress = 60; - int progressMax = 100; - - List<Part> parts = NotificationProgressBar.processModelAndConvertToViewParts(segments, - points, progress, progressMax); - - List<Part> expectedParts = new ArrayList<>( - List.of(new Point(Color.RED), - new Segment(0.25f, Color.RED), - new Point(Color.BLUE), - new Segment(0.25f, Color.RED), - new Segment(0.10f, Color.GREEN), - new Point(Color.BLUE), - new Segment(0.4f, Color.GREEN), - new Point(Color.YELLOW))); - - assertThat(parts).isEqualTo(expectedParts); - - float drawableWidth = 300; + float drawableWidth = 320; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; List<DrawablePart> drawableParts = NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon, + trackerDrawWidth); List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawablePoint(0, 12, Color.RED), - new DrawableSegment(16, 65, Color.RED), - new DrawablePoint(69, 81, Color.BLUE), - new DrawableSegment(85, 146, Color.RED), - new DrawableSegment(150, 170, Color.GREEN), - new DrawablePoint(174, 186, Color.BLUE), - new DrawableSegment(190, 284, Color.GREEN), - new DrawablePoint(288, 300, Color.YELLOW))); + List.of(new DrawableSegment(10, 45, Color.RED), + new DrawablePoint(49, 61, Color.RED), + new DrawableSegment(65, 75, Color.RED), + new DrawablePoint(79, 91, Color.BLUE), + new DrawableSegment(95, 156, Color.RED), + new DrawableSegment(160, 180, Color.GREEN), + new DrawablePoint(184, 196, Color.BLUE), + new DrawableSegment(200, 225, Color.GREEN), + new DrawablePoint(229, 241, Color.YELLOW), + new DrawableSegment(245, 310, Color.GREEN))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); @@ -592,22 +567,24 @@ public class NotificationProgressBarTest { Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 50% opacity int fadedGreen = 0x8000FF00; int fadedYellow = 0x80FFFF00; expectedDrawableParts = new ArrayList<>( - List.of(new DrawablePoint(0, 12, Color.RED), - new DrawableSegment(16, 65, Color.RED), - new DrawablePoint(69, 81, Color.BLUE), - new DrawableSegment(85, 146, Color.RED), - new DrawableSegment(150, 170, Color.GREEN), - new DrawablePoint(174, 186, Color.BLUE), - new DrawableSegment(190, 284, fadedGreen, true), - new DrawablePoint(288, 300, fadedYellow))); - - assertThat(p.second).isEqualTo(180); + List.of(new DrawableSegment(10, 44.095238F, Color.RED), + new DrawablePoint(48.095238F, 60.095238F, Color.RED), + new DrawableSegment(64.095238F, 80.09524F, Color.RED), + new DrawablePoint(84.09524F, 96.09524F, Color.BLUE), + new DrawableSegment(100.09524F, 158.9524F, Color.RED), + new DrawableSegment(162.95238F, 182.7619F, Color.GREEN), + new DrawablePoint(186.7619F, 198.7619F, Color.BLUE), + new DrawableSegment(202.7619F, 227.33333F, fadedGreen, true), + new DrawablePoint(231.33333F, 243.33333F, fadedYellow), + new DrawableSegment(247.33333F, 309.99997F, fadedGreen, true))); + + assertThat(p.second).isEqualTo(192.7619F); assertThat(p.first).isEqualTo(expectedDrawableParts); } @@ -644,27 +621,29 @@ public class NotificationProgressBarTest { assertThat(parts).isEqualTo(expectedParts); - float drawableWidth = 300; + float drawableWidth = 320; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; List<DrawablePart> drawableParts = NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon, + trackerDrawWidth); List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, -7, Color.RED), - new DrawablePoint(-3, 9, Color.RED), - new DrawableSegment(13, 65, Color.RED), - new DrawablePoint(69, 81, Color.BLUE), - new DrawableSegment(85, 146, Color.RED), - new DrawableSegment(150, 170, Color.GREEN), - new DrawablePoint(174, 186, Color.BLUE), - new DrawableSegment(190, 287, Color.GREEN), - new DrawablePoint(291, 303, Color.YELLOW), - new DrawableSegment(307, 300, Color.GREEN))); + List.of(new DrawableSegment(10, 3, Color.RED), + new DrawablePoint(7, 19, Color.RED), + new DrawableSegment(23, 75, Color.RED), + new DrawablePoint(79, 91, Color.BLUE), + new DrawableSegment(95, 156, Color.RED), + new DrawableSegment(160, 180, Color.GREEN), + new DrawablePoint(184, 196, Color.BLUE), + new DrawableSegment(200, 297, Color.GREEN), + new DrawablePoint(301, 313, Color.YELLOW), + new DrawableSegment(317, 310, Color.GREEN))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); @@ -673,24 +652,24 @@ public class NotificationProgressBarTest { Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 50% opacity int fadedGreen = 0x8000FF00; int fadedYellow = 0x80FFFF00; expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 16, Color.RED), - new DrawablePoint(20, 32, Color.RED), - new DrawableSegment(36, 78.02409F, Color.RED), - new DrawablePoint(82.02409F, 94.02409F, Color.BLUE), - new DrawableSegment(98.02409F, 146.55421F, Color.RED), - new DrawableSegment(150.55421F, 169.44579F, Color.GREEN), - new DrawablePoint(173.44579F, 185.44579F, Color.BLUE), - new DrawableSegment(189.44579F, 264, fadedGreen, true), - new DrawablePoint(268, 280, fadedYellow), - new DrawableSegment(284, 300, fadedGreen, true))); - - assertThat(p.second).isEqualTo(179.44579F); + List.of(new DrawableSegment(10, 26, Color.RED), + new DrawablePoint(30, 42, Color.RED), + new DrawableSegment(46, 88.02409F, Color.RED), + new DrawablePoint(92.02409F, 104.02409F, Color.BLUE), + new DrawableSegment(108.02409F, 156.55421F, Color.RED), + new DrawableSegment(160.55421F, 179.44579F, Color.GREEN), + new DrawablePoint(183.44579F, 195.44579F, Color.BLUE), + new DrawableSegment(199.44579F, 274, fadedGreen, true), + new DrawablePoint(278, 290, fadedYellow), + new DrawableSegment(294, 310, fadedGreen, true))); + + assertThat(p.second).isEqualTo(189.44579F); assertThat(p.first).isEqualTo(expectedDrawableParts); } @@ -711,31 +690,38 @@ public class NotificationProgressBarTest { points, progress, progressMax); List<Part> expectedParts = new ArrayList<>( - List.of(new Segment(0.15f, Color.RED), new Point(Color.RED), - new Segment(0.10f, Color.RED), new Point(Color.BLUE), - new Segment(0.25f, Color.RED), new Segment(0.25f, Color.GREEN), - new Point(Color.YELLOW), new Segment(0.25f, Color.GREEN))); + List.of(new Segment(0.15f, Color.RED), + new Point(Color.RED), + new Segment(0.10f, Color.RED), + new Point(Color.BLUE), + new Segment(0.25f, Color.RED), + new Segment(0.25f, Color.GREEN), + new Point(Color.YELLOW), + new Segment(0.25f, Color.GREEN))); assertThat(parts).isEqualTo(expectedParts); - float drawableWidth = 300; + float drawableWidth = 320; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; List<DrawablePart> drawableParts = NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon, + trackerDrawWidth); List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 35, Color.RED), new DrawablePoint(39, 51, Color.RED), - new DrawableSegment(55, 65, Color.RED), - new DrawablePoint(69, 81, Color.BLUE), - new DrawableSegment(85, 146, Color.RED), - new DrawableSegment(150, 215, Color.GREEN), - new DrawablePoint(219, 231, Color.YELLOW), - new DrawableSegment(235, 300, Color.GREEN))); + List.of(new DrawableSegment(10, 45, Color.RED), + new DrawablePoint(49, 61, Color.RED), + new DrawableSegment(65, 75, Color.RED), + new DrawablePoint(79, 91, Color.BLUE), + new DrawableSegment(95, 156, Color.RED), + new DrawableSegment(160, 225, Color.GREEN), + new DrawablePoint(229, 241, Color.YELLOW), + new DrawableSegment(245, 310, Color.GREEN))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); @@ -744,34 +730,34 @@ public class NotificationProgressBarTest { Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 34.296295F, Color.RED), - new DrawablePoint(38.296295F, 50.296295F, Color.RED), - new DrawableSegment(54.296295F, 70.296295F, Color.RED), - new DrawablePoint(74.296295F, 86.296295F, Color.BLUE), - new DrawableSegment(90.296295F, 149.62962F, Color.RED), - new DrawableSegment(153.62962F, 216.8148F, Color.GREEN), - new DrawablePoint(220.81482F, 232.81482F, Color.YELLOW), - new DrawableSegment(236.81482F, 300, Color.GREEN))); - - assertThat(p.second).isEqualTo(182.9037F); + List.of(new DrawableSegment(10, 44.296295F, Color.RED), + new DrawablePoint(48.296295F, 60.296295F, Color.RED), + new DrawableSegment(64.296295F, 80.296295F, Color.RED), + new DrawablePoint(84.296295F, 96.296295F, Color.BLUE), + new DrawableSegment(100.296295F, 159.62962F, Color.RED), + new DrawableSegment(163.62962F, 226.8148F, Color.GREEN), + new DrawablePoint(230.81482F, 242.81482F, Color.YELLOW), + new DrawableSegment(246.81482F, 310, Color.GREEN))); + + assertThat(p.second).isEqualTo(192.9037F); assertThat(p.first).isEqualTo(expectedDrawableParts); } - // The only difference from the `zeroWidthDrawableSegment` test below is the longer + // The only difference from the `segmentWidthAtMin` test below is the longer // segmentMinWidth (= 16dp). @Test - public void maybeStretchAndRescaleSegments_negativeWidthDrawableSegment() + public void maybeStretchAndRescaleSegments_segmentWidthBelowMin() throws NotEnoughWidthToFitAllPartsException { List<ProgressStyle.Segment> segments = new ArrayList<>(); - segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE)); segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE)); List<ProgressStyle.Point> points = new ArrayList<>(); - points.add(new ProgressStyle.Point(0).setColor(Color.BLUE)); + points.add(new ProgressStyle.Point(200).setColor(Color.BLUE)); int progress = 1000; int progressMax = 1000; @@ -779,28 +765,32 @@ public class NotificationProgressBarTest { points, progress, progressMax); List<Part> expectedParts = new ArrayList<>( - List.of(new Point(Color.BLUE), new Segment(0.1f, Color.BLUE), - new Segment(0.2f, Color.BLUE), new Segment(0.3f, Color.BLUE), + List.of(new Segment(0.2f, Color.BLUE), + new Point(Color.BLUE), + new Segment(0.1f, Color.BLUE), + new Segment(0.3f, Color.BLUE), new Segment(0.4f, Color.BLUE))); assertThat(parts).isEqualTo(expectedParts); - float drawableWidth = 200; + float drawableWidth = 220; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; List<DrawablePart> drawableParts = NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon, + trackerDrawWidth); List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawablePoint(0, 12, Color.BLUE), - new DrawableSegment(16, 16, Color.BLUE), - new DrawableSegment(20, 56, Color.BLUE), - new DrawableSegment(60, 116, Color.BLUE), - new DrawableSegment(120, 200, Color.BLUE))); + List.of(new DrawableSegment(10, 40, Color.BLUE), + new DrawablePoint(44, 56, Color.BLUE), + new DrawableSegment(60, 66, Color.BLUE), + new DrawableSegment(70, 126, Color.BLUE), + new DrawableSegment(130, 210, Color.BLUE))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); @@ -809,30 +799,31 @@ public class NotificationProgressBarTest { Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 200, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); - expectedDrawableParts = new ArrayList<>(List.of(new DrawablePoint(0, 12, Color.BLUE), - new DrawableSegment(16, 32, Color.BLUE), - new DrawableSegment(36, 69.41936F, Color.BLUE), - new DrawableSegment(73.41936F, 124.25807F, Color.BLUE), - new DrawableSegment(128.25807F, 200, Color.BLUE))); + expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(10, 38.81356F, Color.BLUE), + new DrawablePoint(42.81356F, 54.81356F, Color.BLUE), + new DrawableSegment(58.81356F, 74.81356F, Color.BLUE), + new DrawableSegment(78.81356F, 131.42374F, Color.BLUE), + new DrawableSegment(135.42374F, 210, Color.BLUE))); - assertThat(p.second).isEqualTo(200); + assertThat(p.second).isEqualTo(210); assertThat(p.first).isEqualTo(expectedDrawableParts); } - // The only difference from the `negativeWidthDrawableSegment` test above is the shorter + // The only difference from the `segmentWidthBelowMin` test above is the shorter // segmentMinWidth (= 10dp). @Test - public void maybeStretchAndRescaleSegments_zeroWidthDrawableSegment() + public void maybeStretchAndRescaleSegments_segmentWidthAtMin() throws NotEnoughWidthToFitAllPartsException { List<ProgressStyle.Segment> segments = new ArrayList<>(); - segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE)); segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE)); List<ProgressStyle.Point> points = new ArrayList<>(); - points.add(new ProgressStyle.Point(0).setColor(Color.BLUE)); + points.add(new ProgressStyle.Point(200).setColor(Color.BLUE)); int progress = 1000; int progressMax = 1000; @@ -840,28 +831,32 @@ public class NotificationProgressBarTest { points, progress, progressMax); List<Part> expectedParts = new ArrayList<>( - List.of(new Point(Color.BLUE), new Segment(0.1f, Color.BLUE), - new Segment(0.2f, Color.BLUE), new Segment(0.3f, Color.BLUE), + List.of(new Segment(0.2f, Color.BLUE), + new Point(Color.BLUE), + new Segment(0.1f, Color.BLUE), + new Segment(0.3f, Color.BLUE), new Segment(0.4f, Color.BLUE))); assertThat(parts).isEqualTo(expectedParts); - float drawableWidth = 200; + float drawableWidth = 220; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; List<DrawablePart> drawableParts = NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon, + trackerDrawWidth); List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawablePoint(0, 12, Color.BLUE), - new DrawableSegment(16, 16, Color.BLUE), - new DrawableSegment(20, 56, Color.BLUE), - new DrawableSegment(60, 116, Color.BLUE), - new DrawableSegment(120, 200, Color.BLUE))); + List.of(new DrawableSegment(10, 40, Color.BLUE), + new DrawablePoint(44, 56, Color.BLUE), + new DrawableSegment(60, 66, Color.BLUE), + new DrawableSegment(70, 126, Color.BLUE), + new DrawableSegment(130, 210, Color.BLUE))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); @@ -870,15 +865,16 @@ public class NotificationProgressBarTest { Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 200, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); - expectedDrawableParts = new ArrayList<>(List.of(new DrawablePoint(0, 12, Color.BLUE), - new DrawableSegment(16, 26, Color.BLUE), - new DrawableSegment(30, 64.169014F, Color.BLUE), - new DrawableSegment(68.169014F, 120.92958F, Color.BLUE), - new DrawableSegment(124.92958F, 200, Color.BLUE))); + expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(10, 39.411766F, Color.BLUE), + new DrawablePoint(43.411766F, 55.411766F, Color.BLUE), + new DrawableSegment(59.411766F, 69.411766F, Color.BLUE), + new DrawableSegment(73.411766F, 128.05884F, Color.BLUE), + new DrawableSegment(132.05882F, 210, Color.BLUE))); - assertThat(p.second).isEqualTo(200); + assertThat(p.second).isEqualTo(210); assertThat(p.first).isEqualTo(expectedDrawableParts); } @@ -886,12 +882,12 @@ public class NotificationProgressBarTest { public void maybeStretchAndRescaleSegments_noStretchingNecessary() throws NotEnoughWidthToFitAllPartsException { List<ProgressStyle.Segment> segments = new ArrayList<>(); - segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE)); segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE)); segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE)); segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE)); List<ProgressStyle.Point> points = new ArrayList<>(); - points.add(new ProgressStyle.Point(0).setColor(Color.BLUE)); + points.add(new ProgressStyle.Point(100).setColor(Color.BLUE)); int progress = 1000; int progressMax = 1000; @@ -899,28 +895,32 @@ public class NotificationProgressBarTest { points, progress, progressMax); List<Part> expectedParts = new ArrayList<>( - List.of(new Point(Color.BLUE), new Segment(0.2f, Color.BLUE), - new Segment(0.1f, Color.BLUE), new Segment(0.3f, Color.BLUE), + List.of(new Segment(0.1f, Color.BLUE), + new Point(Color.BLUE), + new Segment(0.2f, Color.BLUE), + new Segment(0.3f, Color.BLUE), new Segment(0.4f, Color.BLUE))); assertThat(parts).isEqualTo(expectedParts); - float drawableWidth = 200; + float drawableWidth = 220; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; List<DrawablePart> drawableParts = NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon, + trackerDrawWidth); List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawablePoint(0, 12, Color.BLUE), - new DrawableSegment(16, 36, Color.BLUE), - new DrawableSegment(40, 56, Color.BLUE), - new DrawableSegment(60, 116, Color.BLUE), - new DrawableSegment(120, 200, Color.BLUE))); + List.of(new DrawableSegment(10, 20, Color.BLUE), + new DrawablePoint(24, 36, Color.BLUE), + new DrawableSegment(40, 66, Color.BLUE), + new DrawableSegment(70, 126, Color.BLUE), + new DrawableSegment(130, 210, Color.BLUE))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); @@ -929,9 +929,9 @@ public class NotificationProgressBarTest { Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 200, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); - assertThat(p.second).isEqualTo(200); + assertThat(p.second).isEqualTo(210); assertThat(p.first).isEqualTo(expectedDrawableParts); } @@ -951,10 +951,10 @@ public class NotificationProgressBarTest { segments.add(new ProgressStyle.Segment(10).setColor(Color.GREEN)); segments.add(new ProgressStyle.Segment(10).setColor(Color.RED)); List<ProgressStyle.Point> points = new ArrayList<>(); - points.add(new ProgressStyle.Point(0).setColor(orange)); points.add(new ProgressStyle.Point(1).setColor(Color.BLUE)); + points.add(new ProgressStyle.Point(10).setColor(orange)); points.add(new ProgressStyle.Point(55).setColor(Color.BLUE)); - points.add(new ProgressStyle.Point(100).setColor(orange)); + points.add(new ProgressStyle.Point(90).setColor(orange)); int progress = 50; int progressMax = 100; @@ -962,10 +962,10 @@ public class NotificationProgressBarTest { points, progress, progressMax); List<Part> expectedParts = new ArrayList<>( - List.of(new Point(orange), - new Segment(0.01f, orange), + List.of(new Segment(0.01f, orange), new Point(Color.BLUE), new Segment(0.09f, orange), + new Point(orange), new Segment(0.1f, Color.YELLOW), new Segment(0.1f, Color.BLUE), new Segment(0.1f, Color.GREEN), @@ -976,21 +976,23 @@ public class NotificationProgressBarTest { new Segment(0.1f, Color.YELLOW), new Segment(0.1f, Color.BLUE), new Segment(0.1f, Color.GREEN), - new Segment(0.1f, Color.RED), - new Point(orange))); + new Point(orange), + new Segment(0.1f, Color.RED))); assertThat(parts).isEqualTo(expectedParts); // For the list of ProgressStyle.Part used in this test, 300 is the minimum width. - float drawableWidth = 299; + float drawableWidth = 319; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; List<DrawablePart> drawableParts = NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon, + trackerDrawWidth); // Skips the validation of the intermediate list of DrawableParts. @@ -999,7 +1001,7 @@ public class NotificationProgressBarTest { NotificationProgressBar.maybeStretchAndRescaleSegments( parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); } @Test @@ -1015,11 +1017,12 @@ public class NotificationProgressBarTest { int progress = 60; int progressMax = 100; - float drawableWidth = 300; + float drawableWidth = 320; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; float segmentMinWidth = 16; boolean isStyledByProgress = true; @@ -1036,24 +1039,24 @@ public class NotificationProgressBarTest { pointRadius, hasTrackerIcon, segmentMinWidth, - isStyledByProgress - ); + isStyledByProgress, + trackerDrawWidth); // Colors with 50% opacity int fadedBlue = 0x800000FF; int fadedYellow = 0x80FFFF00; List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 34.219177F, Color.BLUE), - new DrawablePoint(38.219177F, 50.219177F, Color.RED), - new DrawableSegment(54.219177F, 70.21918F, Color.BLUE), - new DrawablePoint(74.21918F, 86.21918F, Color.BLUE), - new DrawableSegment(90.21918F, 172.38356F, Color.BLUE), - new DrawablePoint(176.38356F, 188.38356F, Color.BLUE), - new DrawableSegment(192.38356F, 217.0137F, fadedBlue, true), - new DrawablePoint(221.0137F, 233.0137F, fadedYellow), - new DrawableSegment(237.0137F, 300F, fadedBlue, true))); - - assertThat(p.second).isEqualTo(182.38356F); + List.of(new DrawableSegment(10, 44.219177F, Color.BLUE), + new DrawablePoint(48.219177F, 60.219177F, Color.RED), + new DrawableSegment(64.219177F, 80.21918F, Color.BLUE), + new DrawablePoint(84.21918F, 96.21918F, Color.BLUE), + new DrawableSegment(100.21918F, 182.38356F, Color.BLUE), + new DrawablePoint(186.38356F, 198.38356F, Color.BLUE), + new DrawableSegment(202.38356F, 227.0137F, fadedBlue, true), + new DrawablePoint(231.0137F, 243.0137F, fadedYellow), + new DrawableSegment(247.0137F, 310F, fadedBlue, true))); + + assertThat(p.second).isEqualTo(192.38356F); assertThat(p.first).isEqualTo(expectedDrawableParts); } @@ -1065,11 +1068,12 @@ public class NotificationProgressBarTest { int progress = 60; int progressMax = 100; - float drawableWidth = 100; + float drawableWidth = 120; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; float segmentMinWidth = 16; boolean isStyledByProgress = true; @@ -1086,16 +1090,16 @@ public class NotificationProgressBarTest { pointRadius, hasTrackerIcon, segmentMinWidth, - isStyledByProgress - ); + isStyledByProgress, + trackerDrawWidth); - // Colors with 50%f opacity + // Colors with 50% opacity int fadedBlue = 0x800000FF; List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 60.000004F, Color.BLUE), - new DrawableSegment(60.000004F, 100, fadedBlue, true))); + List.of(new DrawableSegment(10, 70F, Color.BLUE), + new DrawableSegment(70F, 110, fadedBlue, true))); - assertThat(p.second).isWithin(1e-5f).of(60); + assertThat(p.second).isEqualTo(70); assertThat(p.first).isEqualTo(expectedDrawableParts); } } diff --git a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressModelTest.java b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressModelTest.java index e1f5b1c2e4a4..140d268e855b 100644 --- a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressModelTest.java +++ b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressModelTest.java @@ -90,7 +90,7 @@ public class NotificationProgressModelTest { new Notification.ProgressStyle.Segment(50).setColor(Color.YELLOW), new Notification.ProgressStyle.Segment(50).setColor(Color.LTGRAY)); final List<Notification.ProgressStyle.Point> points = List.of( - new Notification.ProgressStyle.Point(0).setColor(Color.RED), + new Notification.ProgressStyle.Point(1).setColor(Color.RED), new Notification.ProgressStyle.Point(20).setColor(Color.BLUE)); final NotificationProgressModel savedModel = new NotificationProgressModel(segments, points, @@ -121,7 +121,7 @@ public class NotificationProgressModelTest { new Notification.ProgressStyle.Segment(50).setColor(Color.YELLOW), new Notification.ProgressStyle.Segment(50).setColor(Color.YELLOW)); final List<Notification.ProgressStyle.Point> points = List.of( - new Notification.ProgressStyle.Point(0).setColor(Color.RED), + new Notification.ProgressStyle.Point(1).setColor(Color.RED), new Notification.ProgressStyle.Point(20).setColor(Color.BLUE)); final NotificationProgressModel savedModel = new NotificationProgressModel(segments, points, diff --git a/graphics/java/android/graphics/Bitmap.java b/graphics/java/android/graphics/Bitmap.java index dfded7321b2c..0c4ea79dd5be 100644 --- a/graphics/java/android/graphics/Bitmap.java +++ b/graphics/java/android/graphics/Bitmap.java @@ -102,6 +102,10 @@ public final class Bitmap implements Parcelable { private static volatile int sDefaultDensity = -1; + /** + * This id is not authoritative and can be duplicated if an ashmem bitmap is decoded from a + * parcel. + */ private long mId; /** diff --git a/libs/WindowManager/Shell/OWNERS b/libs/WindowManager/Shell/OWNERS index f01e8d665d12..ab2f3ef94eb6 100644 --- a/libs/WindowManager/Shell/OWNERS +++ b/libs/WindowManager/Shell/OWNERS @@ -1,7 +1,7 @@ -xutan@google.com +jorgegil@google.com pbdr@google.com pragyabajoria@google.com # Give submodule owners in shell resource approval -per-file res*/*/*.xml = atsjenk@google.com, hwwang@google.com, jorgegil@google.com, lbill@google.com, madym@google.com, vaniadesmonda@google.com, pbdr@google.com, mpodolian@google.com, liranb@google.com, pragyabajoria@google.com, uysalorhan@google.com, gsennton@google.com, mattsziklay@google.com, mdehaini@google.com +per-file res*/*/*.xml = atsjenk@google.com, hwwang@google.com, lbill@google.com, madym@google.com, vaniadesmonda@google.com, pbdr@google.com, mpodolian@google.com, liranb@google.com, pragyabajoria@google.com, uysalorhan@google.com, gsennton@google.com, mattsziklay@google.com, mdehaini@google.com per-file res*/*/tv_*.xml = bronger@google.com diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 13d0169c47c5..a08f88a5b937 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -177,3 +177,10 @@ flag { description: "Factor task-view state tracking out of taskviewtransitions" bug: "384976265" } + +flag { + name: "enable_bubble_bar_on_phones" + namespace: "multitasking" + description: "Try out bubble bar on phones" + bug: "394869612" +} diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt index bce6c5999a75..a32ec221e08a 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt @@ -61,7 +61,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import java.util.Optional @@ -133,7 +132,7 @@ class BubbleControllerBubbleBarTest { mainExecutor, bgExecutor, ) - bubbleController.asBubbles().setSysuiProxy(Mockito.mock(SysuiProxy::class.java)) + bubbleController.asBubbles().setSysuiProxy(mock<SysuiProxy>()) shellInit.init() diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt index 88bfeb21bb74..e865111e59dc 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt @@ -50,10 +50,10 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.never +import org.mockito.kotlin.spy import org.mockito.kotlin.verify import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit @@ -635,7 +635,7 @@ class BubbleStackViewTest { @Test fun removeFromWindow_stopMonitoringSwipeUpGesture() { - bubbleStackView = Mockito.spy(bubbleStackView) + bubbleStackView = spy(bubbleStackView) InstrumentationRegistry.getInstrumentation().runOnMainSync { // No way to add to window in the test environment right now so just pretend bubbleStackView.onDetachedFromWindow() diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/UiEventSubjectTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/UiEventSubjectTest.kt index af238d033aee..3499ee32e649 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/UiEventSubjectTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/UiEventSubjectTest.kt @@ -29,7 +29,8 @@ import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.mock +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock import org.mockito.kotlin.whenever /** Test for [UiEventSubject] */ @@ -130,10 +131,10 @@ class UiEventSubjectTest { } private fun createBubble(appUid: Int, packageName: String, instanceId: InstanceId): Bubble { - return mock(Bubble::class.java).apply { - whenever(getAppUid()).thenReturn(appUid) - whenever(getPackageName()).thenReturn(packageName) - whenever(getInstanceId()).thenReturn(instanceId) + return mock<Bubble>() { + on { getAppUid() } doReturn appUid + on { getPackageName() } doReturn packageName + on { getInstanceId() } doReturn instanceId } } diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt index c022a298e972..7b5831376dc0 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt @@ -73,7 +73,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.mock import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -127,7 +126,7 @@ class BubbleBarLayerViewTest { mainExecutor, bgExecutor, ) - bubbleController.asBubbles().setSysuiProxy(mock(SysuiProxy::class.java)) + bubbleController.asBubbles().setSysuiProxy(mock<SysuiProxy>()) // Flush so that proxy gets set mainExecutor.flushAll() diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml index b1fedce5597e..50c08732543a 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml @@ -46,17 +46,10 @@ android:contentDescription="@string/app_icon_text" android:importantForAccessibility="no"/> - <TextView + <com.android.wm.shell.windowdecor.MarqueedTextView android:id="@+id/application_name" - android:layout_width="0dp" - android:layout_height="wrap_content" tools:text="Gmail" - android:textColor="@androidprv:color/materialColorOnSurface" - android:textSize="14sp" - android:textFontWeight="500" - android:lineHeight="20dp" - android:textStyle="normal" - android:layout_weight="1"/> + style="@style/DesktopModeHandleMenuActionButtonTextView"/> <com.android.wm.shell.windowdecor.HandleMenuImageButton android:id="@+id/collapse_menu_button" @@ -133,37 +126,77 @@ android:elevation="@dimen/desktop_mode_handle_menu_pill_elevation" android:background="@drawable/desktop_mode_decor_handle_menu_background"> - <Button + <LinearLayout android:id="@+id/screenshot_button" android:contentDescription="@string/screenshot_text" - android:text="@string/screenshot_text" - android:drawableStart="@drawable/desktop_mode_ic_handle_menu_screenshot" - android:drawableTint="@androidprv:color/materialColorOnSurface" - style="@style/DesktopModeHandleMenuActionButton"/> + style="@style/DesktopModeHandleMenuActionButtonLayout"> + + <ImageView + android:id="@+id/image" + android:src="@drawable/desktop_mode_ic_handle_menu_screenshot" + android:importantForAccessibility="no" + style="@style/DesktopModeHandleMenuActionButtonImage"/> + + <com.android.wm.shell.windowdecor.MarqueedTextView + android:id="@+id/label" + android:text="@string/screenshot_text" + style="@style/DesktopModeHandleMenuActionButtonTextView"/> - <Button + </LinearLayout> + + <LinearLayout android:id="@+id/new_window_button" android:contentDescription="@string/new_window_text" - android:text="@string/new_window_text" - android:drawableStart="@drawable/desktop_mode_ic_handle_menu_new_window" - android:drawableTint="@androidprv:color/materialColorOnSurface" - style="@style/DesktopModeHandleMenuActionButton" /> + style="@style/DesktopModeHandleMenuActionButtonLayout"> + + <ImageView + android:id="@+id/image" + android:src="@drawable/desktop_mode_ic_handle_menu_new_window" + android:importantForAccessibility="no" + style="@style/DesktopModeHandleMenuActionButtonImage"/> - <Button + <com.android.wm.shell.windowdecor.MarqueedTextView + android:id="@+id/label" + android:text="@string/new_window_text" + style="@style/DesktopModeHandleMenuActionButtonTextView"/> + + </LinearLayout> + + <LinearLayout android:id="@+id/manage_windows_button" android:contentDescription="@string/manage_windows_text" - android:text="@string/manage_windows_text" - android:drawableStart="@drawable/desktop_mode_ic_handle_menu_manage_windows" - android:drawableTint="@androidprv:color/materialColorOnSurface" - style="@style/DesktopModeHandleMenuActionButton" /> + style="@style/DesktopModeHandleMenuActionButtonLayout"> + + <ImageView + android:id="@+id/image" + android:src="@drawable/desktop_mode_ic_handle_menu_manage_windows" + android:importantForAccessibility="no" + style="@style/DesktopModeHandleMenuActionButtonImage"/> + + <com.android.wm.shell.windowdecor.MarqueedTextView + android:id="@+id/label" + android:text="@string/manage_windows_text" + style="@style/DesktopModeHandleMenuActionButtonTextView"/> - <Button + </LinearLayout> + + <LinearLayout android:id="@+id/change_aspect_ratio_button" android:contentDescription="@string/change_aspect_ratio_text" - android:text="@string/change_aspect_ratio_text" - android:drawableStart="@drawable/desktop_mode_ic_handle_menu_change_aspect_ratio" - android:drawableTint="@androidprv:color/materialColorOnSurface" - style="@style/DesktopModeHandleMenuActionButton" /> + style="@style/DesktopModeHandleMenuActionButtonLayout"> + + <ImageView + android:id="@+id/image" + android:src="@drawable/desktop_mode_ic_handle_menu_change_aspect_ratio" + android:importantForAccessibility="no" + style="@style/DesktopModeHandleMenuActionButtonImage"/> + + <com.android.wm.shell.windowdecor.MarqueedTextView + android:id="@+id/label" + android:text="@string/change_aspect_ratio_text" + style="@style/DesktopModeHandleMenuActionButtonTextView"/> + + </LinearLayout> </LinearLayout> <LinearLayout @@ -176,22 +209,37 @@ android:elevation="@dimen/desktop_mode_handle_menu_pill_elevation" android:background="@drawable/desktop_mode_decor_handle_menu_background"> - <Button + <LinearLayout android:id="@+id/open_in_app_or_browser_button" + android:layout_width="0dp" + android:layout_height="match_parent" android:layout_weight="1" + android:layout_marginEnd="8dp" + android:gravity="start|center_vertical" + android:paddingStart="16dp" android:contentDescription="@string/open_in_browser_text" - android:text="@string/open_in_browser_text" - android:drawableStart="@drawable/desktop_mode_ic_handle_menu_open_in_browser" - android:drawableTint="@androidprv:color/materialColorOnSurface" - style="@style/DesktopModeHandleMenuActionButton"/> + android:background="?android:selectableItemBackground"> + + <ImageView + android:id="@+id/image" + android:src="@drawable/desktop_mode_ic_handle_menu_open_in_browser" + android:importantForAccessibility="no" + style="@style/DesktopModeHandleMenuActionButtonImage"/> + + <com.android.wm.shell.windowdecor.MarqueedTextView + android:id="@+id/label" + android:text="@string/open_in_browser_text" + style="@style/DesktopModeHandleMenuActionButtonTextView"/> + + </LinearLayout> <ImageButton android:id="@+id/open_by_default_button" android:layout_width="20dp" android:layout_height="20dp" android:layout_gravity="end|center_vertical" + android:layout_marginStart="8dp" android:layout_marginEnd="16dp" - android:layout_marginStart="10dp" android:contentDescription="@string/open_by_default_settings_text" android:src="@drawable/desktop_mode_ic_handle_menu_open_by_default_settings" android:tint="@androidprv:color/materialColorOnSurface"/> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index a2231dd64112..1b7daa87064a 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -290,7 +290,7 @@ <!-- Accessibility text for the handle fullscreen button [CHAR LIMIT=NONE] --> <string name="fullscreen_text">Fullscreen</string> <!-- Accessibility text for the handle desktop button [CHAR LIMIT=NONE] --> - <string name="desktop_text">Desktop Mode</string> + <string name="desktop_text">Desktop View</string> <!-- Accessibility text for the handle split screen button [CHAR LIMIT=NONE] --> <string name="split_screen_text">Split Screen</string> <!-- Accessibility text for the handle more options button [CHAR LIMIT=NONE] --> @@ -316,7 +316,7 @@ <!-- Accessibility text for the handle menu close menu button [CHAR LIMIT=NONE] --> <string name="collapse_menu_text">Close Menu</string> <!-- Accessibility text for the App Header's App Chip [CHAR LIMIT=NONE] --> - <string name="desktop_mode_app_header_chip_text">Open Menu</string> + <string name="desktop_mode_app_header_chip_text"><xliff:g id="app_name" example="Chrome">%1$s</xliff:g> (Desktop View)</string> <!-- Maximize menu maximize button string. --> <string name="desktop_mode_maximize_menu_maximize_text">Maximize Screen</string> <!-- Maximize menu snap buttons string. --> @@ -342,10 +342,10 @@ <!-- Accessibility text for the Maximize Menu's snap maximize/restore [CHAR LIMIT=NONE] --> <string name="desktop_mode_a11y_action_maximize_restore">Maximize or restore window size</string> - <!-- Accessibility action replacement for caption handle menu split screen button [CHAR LIMIT=NONE] --> - <string name="app_handle_menu_talkback_split_screen_mode_button_text">Enter split screen mode</string> - <!-- Accessibility action replacement for caption handle menu enter desktop mode button [CHAR LIMIT=NONE] --> - <string name="app_handle_menu_talkback_desktop_mode_button_text">Enter desktop windowing mode</string> + <!-- Accessibility action replacement for caption handle app chip buttons [CHAR LIMIT=NONE] --> + <string name="app_handle_chip_accessibility_announce">Open Menu</string> + <!-- Accessibility action replacement for caption handle menu buttons [CHAR LIMIT=NONE] --> + <string name="app_handle_menu_accessibility_announce">Enter <xliff:g id="windowing_mode" example="Desktop View">%1$s</xliff:g></string> <!-- Accessibility action replacement for maximize menu enter snap left button [CHAR LIMIT=NONE] --> <string name="maximize_menu_talkback_action_snap_left_text">Resize window to left</string> <!-- Accessibility action replacement for maximize menu enter snap right button [CHAR LIMIT=NONE] --> diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml index 4ebb7dc6ff37..035004bfd322 100644 --- a/libs/WindowManager/Shell/res/values/styles.xml +++ b/libs/WindowManager/Shell/res/values/styles.xml @@ -40,19 +40,34 @@ <item name="android:activityCloseExitAnimation">@anim/forced_resizable_exit</item> </style> - <style name="DesktopModeHandleMenuActionButton"> + <style name="DesktopModeHandleMenuActionButtonLayout"> <item name="android:layout_width">match_parent</item> <item name="android:layout_height">52dp</item> + <item name="android:layout_weight">1</item> <item name="android:gravity">start|center_vertical</item> - <item name="android:paddingStart">16dp</item> - <item name="android:paddingEnd">0dp</item> - <item name="android:textSize">14sp</item> - <item name="android:textFontWeight">500</item> - <item name="android:textColor">@androidprv:color/materialColorOnSurface</item> - <item name="android:drawablePadding">16dp</item> + <item name="android:paddingHorizontal">16dp</item> <item name="android:background">?android:selectableItemBackground</item> </style> + <style name="DesktopModeHandleMenuActionButtonImage"> + <item name="android:layout_width">20dp</item> + <item name="android:layout_height">20dp</item> + <item name="android:layout_marginEnd">16dp</item> + </style> + + <style name="DesktopModeHandleMenuActionButtonTextView"> + <item name="android:layout_width">0dp</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:layout_weight">1</item> + <item name="android:textSize">14sp</item> + <item name="android:lineHeight">20sp</item> + <item name="android:textFontWeight">500</item> + <item name="android:textColor">@androidprv:color/materialColorOnSurface</item> + <item name="android:ellipsize">marquee</item> + <item name="android:scrollHorizontally">true</item> + <item name="android:singleLine">true</item> + </style> + <style name="DesktopModeHandleMenuWindowingButton"> <item name="android:layout_width">48dp</item> <item name="android:layout_height">48dp</item> diff --git a/libs/WindowManager/Shell/shared/res/values/dimen.xml b/libs/WindowManager/Shell/shared/res/values/dimen.xml index 0b1f76f5ce0e..d280083ae7f5 100644 --- a/libs/WindowManager/Shell/shared/res/values/dimen.xml +++ b/libs/WindowManager/Shell/shared/res/values/dimen.xml @@ -17,4 +17,23 @@ <resources> <dimen name="floating_dismiss_icon_size">32dp</dimen> <dimen name="floating_dismiss_background_size">96dp</dimen> + + <!-- Bubble drag zone dimensions --> + <dimen name="drag_zone_dismiss_fold">140dp</dimen> + <dimen name="drag_zone_dismiss_tablet">200dp</dimen> + <dimen name="drag_zone_bubble_fold">140dp</dimen> + <dimen name="drag_zone_bubble_tablet">200dp</dimen> + <dimen name="drag_zone_full_screen_width">512dp</dimen> + <dimen name="drag_zone_full_screen_height">44dp</dimen> + <dimen name="drag_zone_desktop_window_width">880dp</dimen> + <dimen name="drag_zone_desktop_window_height">300dp</dimen> + <dimen name="drag_zone_desktop_window_expanded_view_width">200dp</dimen> + <dimen name="drag_zone_desktop_window_expanded_view_height">350dp</dimen> + <dimen name="drag_zone_split_from_bubble_height">100dp</dimen> + <dimen name="drag_zone_split_from_bubble_width">60dp</dimen> + <dimen name="drag_zone_h_split_from_expanded_view_width">60dp</dimen> + <dimen name="drag_zone_v_split_from_expanded_view_width">200dp</dimen> + <dimen name="drag_zone_v_split_from_expanded_view_height_tablet">285dp</dimen> + <dimen name="drag_zone_v_split_from_expanded_view_height_fold_tall">150dp</dimen> + <dimen name="drag_zone_v_split_from_expanded_view_height_fold_short">100dp</dimen> </resources>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt index aa523f57c469..909e9d2c4428 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt @@ -16,11 +16,15 @@ package com.android.wm.shell.shared.bubbles +import android.content.Context import android.graphics.Rect +import androidx.annotation.DimenRes +import com.android.wm.shell.shared.R import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker.SplitScreenMode /** A class for creating drag zones for dragging bubble objects or dragging into bubbles. */ class DragZoneFactory( + private val context: Context, private val deviceConfig: DeviceConfig, private val splitScreenModeChecker: SplitScreenModeChecker, private val desktopWindowModeChecker: DesktopWindowModeChecker, @@ -29,23 +33,65 @@ class DragZoneFactory( private val windowBounds: Rect get() = deviceConfig.windowBounds - // TODO b/393172431: move these to xml - private val dismissDragZoneSize = if (deviceConfig.isSmallTablet) 140 else 200 - private val bubbleDragZoneTabletSize = 200 - private val bubbleDragZoneFoldableSize = 140 - private val fullScreenDragZoneWidth = 512 - private val fullScreenDragZoneHeight = 44 - private val desktopWindowDragZoneWidth = 880 - private val desktopWindowDragZoneHeight = 300 - private val desktopWindowFromExpandedViewDragZoneWidth = 200 - private val desktopWindowFromExpandedViewDragZoneHeight = 350 - private val splitFromBubbleDragZoneHeight = 100 - private val splitFromBubbleDragZoneWidth = 60 - private val hSplitFromExpandedViewDragZoneWidth = 60 - private val vSplitFromExpandedViewDragZoneWidth = 200 - private val vSplitFromExpandedViewDragZoneHeightTablet = 285 - private val vSplitFromExpandedViewDragZoneHeightFoldTall = 150 - private val vSplitFromExpandedViewDragZoneHeightFoldShort = 100 + private var dismissDragZoneSize = 0 + private var bubbleDragZoneTabletSize = 0 + private var bubbleDragZoneFoldableSize = 0 + private var fullScreenDragZoneWidth = 0 + private var fullScreenDragZoneHeight = 0 + private var desktopWindowDragZoneWidth = 0 + private var desktopWindowDragZoneHeight = 0 + private var desktopWindowFromExpandedViewDragZoneWidth = 0 + private var desktopWindowFromExpandedViewDragZoneHeight = 0 + private var splitFromBubbleDragZoneHeight = 0 + private var splitFromBubbleDragZoneWidth = 0 + private var hSplitFromExpandedViewDragZoneWidth = 0 + private var vSplitFromExpandedViewDragZoneWidth = 0 + private var vSplitFromExpandedViewDragZoneHeightTablet = 0 + private var vSplitFromExpandedViewDragZoneHeightFoldTall = 0 + private var vSplitFromExpandedViewDragZoneHeightFoldShort = 0 + + init { + onConfigurationUpdated() + } + + /** Updates all dimensions after a configuration change. */ + fun onConfigurationUpdated() { + dismissDragZoneSize = + if (deviceConfig.isSmallTablet) { + context.resolveDimension(R.dimen.drag_zone_dismiss_fold) + } else { + context.resolveDimension(R.dimen.drag_zone_dismiss_tablet) + } + bubbleDragZoneTabletSize = context.resolveDimension(R.dimen.drag_zone_bubble_tablet) + bubbleDragZoneFoldableSize = context.resolveDimension(R.dimen.drag_zone_bubble_fold) + fullScreenDragZoneWidth = context.resolveDimension(R.dimen.drag_zone_full_screen_width) + fullScreenDragZoneHeight = context.resolveDimension(R.dimen.drag_zone_full_screen_height) + desktopWindowDragZoneWidth = + context.resolveDimension(R.dimen.drag_zone_desktop_window_width) + desktopWindowDragZoneHeight = + context.resolveDimension(R.dimen.drag_zone_desktop_window_height) + desktopWindowFromExpandedViewDragZoneWidth = + context.resolveDimension(R.dimen.drag_zone_desktop_window_expanded_view_width) + desktopWindowFromExpandedViewDragZoneHeight = + context.resolveDimension(R.dimen.drag_zone_desktop_window_expanded_view_height) + splitFromBubbleDragZoneHeight = + context.resolveDimension(R.dimen.drag_zone_split_from_bubble_height) + splitFromBubbleDragZoneWidth = + context.resolveDimension(R.dimen.drag_zone_split_from_bubble_width) + hSplitFromExpandedViewDragZoneWidth = + context.resolveDimension(R.dimen.drag_zone_h_split_from_expanded_view_width) + vSplitFromExpandedViewDragZoneWidth = + context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_width) + vSplitFromExpandedViewDragZoneHeightTablet = + context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_height_tablet) + vSplitFromExpandedViewDragZoneHeightFoldTall = + context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_height_fold_tall) + vSplitFromExpandedViewDragZoneHeightFoldShort = + context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_height_fold_short) + } + + private fun Context.resolveDimension(@DimenRes dimension: Int) = + resources.getDimensionPixelSize(dimension) /** * Creates the list of drag zones for the dragged object. @@ -58,11 +104,11 @@ class DragZoneFactory( when (draggedObject) { is DraggedObject.BubbleBar -> { dragZones.add(createDismissDragZone()) - dragZones.addAll(createBubbleDragZones()) + dragZones.addAll(createBubbleHalfScreenDragZones()) } is DraggedObject.Bubble -> { dragZones.add(createDismissDragZone()) - dragZones.addAll(createBubbleDragZones()) + dragZones.addAll(createBubbleCornerDragZones()) dragZones.add(createFullScreenDragZone()) if (shouldShowDesktopWindowDragZones()) { dragZones.add(createDesktopWindowDragZoneForBubble()) @@ -80,7 +126,7 @@ class DragZoneFactory( } else { dragZones.addAll(createSplitScreenDragZonesForExpandedViewOnTablet()) } - createBubbleDragZonesForExpandedView() + dragZones.addAll(createBubbleHalfScreenDragZones()) } } return dragZones @@ -98,7 +144,7 @@ class DragZoneFactory( ) } - private fun createBubbleDragZones(): List<DragZone> { + private fun createBubbleCornerDragZones(): List<DragZone> { val dragZoneSize = if (deviceConfig.isSmallTablet) { bubbleDragZoneFoldableSize @@ -124,7 +170,7 @@ class DragZoneFactory( ) } - private fun createBubbleDragZonesForExpandedView(): List<DragZone> { + private fun createBubbleHalfScreenDragZones(): List<DragZone> { return listOf( DragZone.Bubble.Left( bounds = Rect(0, 0, windowBounds.right / 2, windowBounds.bottom), diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt new file mode 100644 index 000000000000..29ce8d90e66f --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt @@ -0,0 +1,78 @@ +/* + * 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.shared.bubbles + +/** + * Manages animating drop targets in response to dragging bubble icons or bubble expanded views + * across different drag zones. + */ +class DropTargetManager( + private val isLayoutRtl: Boolean, + private val dragZoneChangedListener: DragZoneChangedListener +) { + + private var state: DragState? = null + + /** Must be called when a drag gesture is starting. */ + fun onDragStarted(draggedObject: DraggedObject, dragZones: List<DragZone>) { + val state = DragState(dragZones, draggedObject) + dragZoneChangedListener.onInitialDragZoneSet(state.initialDragZone) + this.state = state + } + + /** Called when the user drags to a new location. */ + fun onDragUpdated(x: Int, y: Int) { + val state = state ?: return + val oldDragZone = state.currentDragZone + val newDragZone = state.getMatchingDragZone(x = x, y = y) + state.currentDragZone = newDragZone + if (oldDragZone != newDragZone) { + dragZoneChangedListener.onDragZoneChanged(from = oldDragZone, to = newDragZone) + } + } + + /** Called when the drag ended. */ + fun onDragEnded() { + state = null + } + + /** Stores the current drag state. */ + private inner class DragState( + private val dragZones: List<DragZone>, + draggedObject: DraggedObject + ) { + val initialDragZone = + if (draggedObject.initialLocation.isOnLeft(isLayoutRtl)) { + dragZones.filterIsInstance<DragZone.Bubble.Left>().first() + } else { + dragZones.filterIsInstance<DragZone.Bubble.Right>().first() + } + var currentDragZone: DragZone = initialDragZone + + fun getMatchingDragZone(x: Int, y: Int): DragZone { + return dragZones.firstOrNull { it.contains(x, y) } ?: currentDragZone + } + } + + /** An interface to be notified when drag zones change. */ + interface DragZoneChangedListener { + /** An initial drag zone was set. Called when a drag starts. */ + fun onInitialDragZoneSet(dragZone: DragZone) + /** Called when the object was dragged to a different drag zone. */ + fun onDragZoneChanged(from: DragZone, to: DragZone) + } +} diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt index f234ff5c2c84..c545d3001cc7 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt @@ -21,6 +21,7 @@ import android.content.Context import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED import android.content.pm.ActivityInfo.OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION +import android.content.pm.ActivityInfo.OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS import android.window.DesktopModeFlags import com.android.internal.R import com.android.window.flags.Flags @@ -59,13 +60,16 @@ class DesktopModeCompatPolicy(private val context: Context) { * The treatment is enabled when all the of the following is true: * * Any flags to forcibly consume caption insets are enabled. * * Top activity have configuration coupled with insets. - * * Task is not resizeable. + * * Task is not resizeable or [ActivityInfo.OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS] + * is enabled. */ fun shouldExcludeCaptionFromAppBounds(taskInfo: TaskInfo): Boolean = Flags.excludeCaptionFromAppBounds() && isAnyForceConsumptionFlagsEnabled() && taskInfo.topActivityInfo?.let { - isInsetsCoupledWithConfiguration(it) && !taskInfo.isResizeable + isInsetsCoupledWithConfiguration(it) && (!taskInfo.isResizeable || it.isChangeEnabled( + OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS + )) } ?: false /** diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java index 2586bd6d86cb..643c1506e4c2 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java @@ -220,6 +220,13 @@ public class DesktopModeStatus { } /** + * Return {@code true} if the current device can host desktop sessions on its internal display. + */ + public static boolean canInternalDisplayHostDesktops(@NonNull Context context) { + return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops); + } + + /** * Return {@code true} if desktop mode dev option should be shown on current device */ public static boolean canShowDesktopModeDevOption(@NonNull Context context) { @@ -231,21 +238,24 @@ public class DesktopModeStatus { * Return {@code true} if desktop mode dev option should be shown on current device */ public static boolean canShowDesktopExperienceDevOption(@NonNull Context context) { - return Flags.showDesktopExperienceDevOption() && isDeviceEligibleForDesktopMode(context); + return Flags.showDesktopExperienceDevOption() + && isInternalDisplayEligibleToHostDesktops(context); } /** Returns if desktop mode dev option should be enabled if there is no user override. */ public static boolean shouldDevOptionBeEnabledByDefault(Context context) { - return isDeviceEligibleForDesktopMode(context) && Flags.enableDesktopWindowingMode(); + return isInternalDisplayEligibleToHostDesktops(context) + && Flags.enableDesktopWindowingMode(); } /** * Return {@code true} if desktop mode is enabled and can be entered on the current device. */ public static boolean canEnterDesktopMode(@NonNull Context context) { - return (isDeviceEligibleForDesktopMode(context) - && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue()) - || isDesktopModeEnabledByDevOption(context); + return (isInternalDisplayEligibleToHostDesktops(context) + && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue() + && (isDesktopModeSupported(context) || !enforceDeviceRestrictions()) + || isDesktopModeEnabledByDevOption(context)); } /** @@ -313,10 +323,11 @@ public class DesktopModeStatus { } /** - * Return {@code true} if desktop mode is unrestricted and is supported in the device. + * Return {@code true} if desktop sessions is unrestricted and can be host for the device's + * internal display. */ - public static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) { - return !enforceDeviceRestrictions() || isDesktopModeSupported(context) || ( + public static boolean isInternalDisplayEligibleToHostDesktops(@NonNull Context context) { + return !enforceDeviceRestrictions() || canInternalDisplayHostDesktops(context) || ( Flags.enableDesktopModeThroughDevOption() && isDesktopModeDevOptionSupported( context)); } @@ -325,7 +336,7 @@ public class DesktopModeStatus { * Return {@code true} if the developer option for desktop mode is unrestricted and is supported * in the device. * - * Note that, if {@link #isDeviceEligibleForDesktopMode(Context)} is true, then + * Note that, if {@link #isInternalDisplayEligibleToHostDesktops(Context)} is true, then * {@link #isDeviceEligibleForDesktopModeDevOption(Context)} is also true. */ private static boolean isDeviceEligibleForDesktopModeDevOption(@NonNull Context context) { diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/draganddrop/DragAndDropConstants.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/draganddrop/DragAndDropConstants.java index 4127adc1f901..12938db07ece 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/draganddrop/DragAndDropConstants.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/draganddrop/DragAndDropConstants.java @@ -24,4 +24,9 @@ public class DragAndDropConstants { * ignore drag events. */ public static final String EXTRA_DISALLOW_HIT_REGION = "DISALLOW_HIT_REGION"; + + /** + * An Intent extra that Launcher can use to specify the {@link android.content.pm.ShortcutInfo} + */ + public static final String EXTRA_SHORTCUT_INFO = "EXTRA_SHORTCUT_INFO"; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index ddcdf9f8c617..d9489287ff42 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -54,6 +54,7 @@ import com.android.launcher3.icons.BubbleIconFactory; import com.android.wm.shell.Flags; import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView; import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; +import com.android.wm.shell.common.ComponentUtils; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.shared.bubbles.BubbleInfo; @@ -282,6 +283,29 @@ public class Bubble implements BubbleViewProvider { mPackageName = intent.getPackage(); } + private Bubble( + PendingIntent intent, + UserHandle user, + String key, + @ShellMainThread Executor mainExecutor, + @ShellBackgroundThread Executor bgExecutor) { + mGroupKey = null; + mLocusId = null; + mFlags = 0; + mUser = user; + mIcon = null; + mType = BubbleType.TYPE_APP; + mKey = key; + mShowBubbleUpdateDot = false; + mMainExecutor = mainExecutor; + mBgExecutor = bgExecutor; + mTaskId = INVALID_TASK_ID; + mPendingIntent = intent; + mIntent = null; + mDesiredHeight = Integer.MAX_VALUE; + mPackageName = ComponentUtils.getPackageName(intent); + } + private Bubble(ShortcutInfo info, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { mGroupKey = null; @@ -336,6 +360,15 @@ public class Bubble implements BubbleViewProvider { } /** Creates an app bubble. */ + public static Bubble createAppBubble(PendingIntent intent, UserHandle user, + @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { + return new Bubble(intent, + user, + /* key= */ getAppBubbleKeyForApp(ComponentUtils.getPackageName(intent), user), + mainExecutor, bgExecutor); + } + + /** Creates an app bubble. */ public static Bubble createAppBubble(Intent intent, UserHandle user, @Nullable Icon icon, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { return new Bubble(intent, 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 b93b7b86e661..c7a0401c2b88 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 @@ -46,6 +46,7 @@ import android.app.NotificationChannel; import android.app.PendingIntent; import android.app.TaskInfo; import android.content.BroadcastReceiver; +import android.content.ClipDescription; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -118,6 +119,7 @@ import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import com.android.wm.shell.shared.bubbles.BubbleBarUpdate; import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider; import com.android.wm.shell.shared.bubbles.DeviceConfig; +import com.android.wm.shell.shared.draganddrop.DragAndDropConstants; import com.android.wm.shell.sysui.ConfigurationChangeListener; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; @@ -791,15 +793,21 @@ public class BubbleController implements ConfigurationChangeListener, public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation, @BubbleBarLocation.UpdateSource int source) { if (isShowingAsBubbleBar()) { + updateExpandedViewForBubbleBarLocation(bubbleBarLocation, source); + BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate(); + bubbleBarUpdate.bubbleBarLocation = bubbleBarLocation; + mBubbleStateListener.onBubbleStateChange(bubbleBarUpdate); + } + } + + private void updateExpandedViewForBubbleBarLocation(BubbleBarLocation bubbleBarLocation, + @BubbleBarLocation.UpdateSource int source) { + if (isShowingAsBubbleBar()) { BubbleBarLocation previousLocation = mBubblePositioner.getBubbleBarLocation(); mBubblePositioner.setBubbleBarLocation(bubbleBarLocation); if (mLayerView != null && !mLayerView.isExpandedViewDragged()) { mLayerView.updateExpandedView(); } - BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate(); - bubbleBarUpdate.bubbleBarLocation = bubbleBarLocation; - mBubbleStateListener.onBubbleStateChange(bubbleBarUpdate); - logBubbleBarLocationIfChanged(bubbleBarLocation, previousLocation, source); } } @@ -872,11 +880,20 @@ public class BubbleController implements ConfigurationChangeListener, } @Override - public void onItemDroppedOverBubbleBarDragZone(BubbleBarLocation location, Intent appIntent, - UserHandle userHandle) { - if (isShowingAsBubbleBar() && BubbleAnythingFlagHelper.enableCreateAnyBubble()) { - hideBubbleBarExpandedViewDropTarget(); - expandStackAndSelectBubble(appIntent, userHandle, location); + public void onItemDroppedOverBubbleBarDragZone(@NonNull BubbleBarLocation location, + Intent itemIntent) { + hideBubbleBarExpandedViewDropTarget(); + ShortcutInfo shortcutInfo = (ShortcutInfo) itemIntent + .getExtra(DragAndDropConstants.EXTRA_SHORTCUT_INFO); + if (shortcutInfo != null) { + expandStackAndSelectBubble(shortcutInfo, location); + return; + } + UserHandle user = (UserHandle) itemIntent.getExtra(Intent.EXTRA_USER); + PendingIntent pendingIntent = (PendingIntent) itemIntent + .getExtra(ClipDescription.EXTRA_PENDING_INTENT); + if (pendingIntent != null && user != null) { + expandStackAndSelectBubble(pendingIntent, user, location); } } @@ -1506,16 +1523,24 @@ public class BubbleController implements ConfigurationChangeListener, * Expands and selects a bubble created or found via the provided shortcut info. * * @param info the shortcut info for the bubble. + * @param bubbleBarLocation optional location in case bubble bar should be repositioned. */ - public void expandStackAndSelectBubble(ShortcutInfo info) { + public void expandStackAndSelectBubble(ShortcutInfo info, + @Nullable BubbleBarLocation bubbleBarLocation) { if (!BubbleAnythingFlagHelper.enableCreateAnyBubble()) return; + BubbleBarLocation updateLocation = isShowingAsBubbleBar() ? bubbleBarLocation : null; + if (updateLocation != null) { + updateExpandedViewForBubbleBarLocation(updateLocation, + BubbleBarLocation.UpdateSource.APP_ICON_DRAG); + } Bubble b = mBubbleData.getOrCreateBubble(info); // Removes from overflow ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - shortcut=%s", info); if (b.isInflated()) { - mBubbleData.setSelectedBubbleAndExpandStack(b); + mBubbleData.setSelectedBubbleAndExpandStack(b, updateLocation); } else { b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); - inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); + inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false, + updateLocation); } } @@ -1524,14 +1549,8 @@ public class BubbleController implements ConfigurationChangeListener, * * @param intent the intent for the bubble. */ - public void expandStackAndSelectBubble(Intent intent, UserHandle user, - @Nullable BubbleBarLocation bubbleBarLocation) { + public void expandStackAndSelectBubble(Intent intent, UserHandle user) { if (!BubbleAnythingFlagHelper.enableCreateAnyBubble()) return; - if (bubbleBarLocation != null) { - //TODO (b/388894910) combine location update with the setSelectedBubbleAndExpandStack & - // fix bubble bar flicking - setBubbleBarLocation(bubbleBarLocation, BubbleBarLocation.UpdateSource.APP_ICON_DRAG); - } Bubble b = mBubbleData.getOrCreateBubble(intent, user); // Removes from overflow ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - intent=%s", intent); if (b.isInflated()) { @@ -1543,6 +1562,31 @@ public class BubbleController implements ConfigurationChangeListener, } /** + * Expands and selects a bubble created or found for this app. + * + * @param pendingIntent the intent for the bubble. + * @param bubbleBarLocation optional location in case bubble bar should be repositioned. + */ + public void expandStackAndSelectBubble(PendingIntent pendingIntent, UserHandle user, + @Nullable BubbleBarLocation bubbleBarLocation) { + if (!BubbleAnythingFlagHelper.enableCreateAnyBubble()) return; + BubbleBarLocation updateLocation = isShowingAsBubbleBar() ? bubbleBarLocation : null; + if (updateLocation != null) { + updateExpandedViewForBubbleBarLocation(updateLocation, + BubbleBarLocation.UpdateSource.APP_ICON_DRAG); + } + Bubble b = mBubbleData.getOrCreateBubble(pendingIntent, user); + ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - pendingIntent=%s", + pendingIntent); + if (b.isInflated()) { + mBubbleData.setSelectedBubbleAndExpandStack(b, updateLocation); + } else { + b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); + inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false, updateLocation); + } + } + + /** * Expands and selects a bubble created from a running task in a different mode. * * @param taskInfo the task. @@ -1904,11 +1948,22 @@ public class BubbleController implements ConfigurationChangeListener, @VisibleForTesting public void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade) { + inflateAndAdd(bubble, suppressFlyout, showInShade, /* bubbleBarLocation= */ null); + } + + /** + * Inflates and adds a bubble. Updates Bubble Bar location if bubbles + * are shown in the Bubble Bar and the location is not null. + */ + @VisibleForTesting + public void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade, + @Nullable BubbleBarLocation bubbleBarLocation) { // Lazy init stack view when a bubble is created ensureBubbleViewsAndWindowCreated(); bubble.setInflateSynchronously(mInflateSynchronously); bubble.inflate( - b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade), + b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade, + bubbleBarLocation), mContext, mExpandedViewManager, mBubbleTaskViewFactory, @@ -2242,7 +2297,8 @@ public class BubbleController implements ConfigurationChangeListener, ProtoLog.d(WM_SHELL_BUBBLES, "mBubbleDataListener#applyUpdate:" + " added=%s removed=%b updated=%s orderChanged=%b expansionChanged=%b" + " expanded=%b selectionChanged=%b selected=%s" - + " suppressed=%s unsupressed=%s shouldShowEducation=%b showOverflowChanged=%b", + + " suppressed=%s unsupressed=%s shouldShowEducation=%b showOverflowChanged=%b" + + " bubbleBarLocation=%s", update.addedBubble != null ? update.addedBubble.getKey() : "null", !update.removedBubbles.isEmpty(), update.updatedBubble != null ? update.updatedBubble.getKey() : "null", @@ -2251,7 +2307,9 @@ public class BubbleController implements ConfigurationChangeListener, update.selectedBubble != null ? update.selectedBubble.getKey() : "null", update.suppressedBubble != null ? update.suppressedBubble.getKey() : "null", update.unsuppressedBubble != null ? update.unsuppressedBubble.getKey() : "null", - update.shouldShowEducation, update.showOverflowChanged); + update.shouldShowEducation, update.showOverflowChanged, + update.mBubbleBarLocation != null ? update.mBubbleBarLocation.toString() + : "null"); ensureBubbleViewsAndWindowCreated(); @@ -2756,13 +2814,13 @@ public class BubbleController implements ConfigurationChangeListener, @Override public void showShortcutBubble(ShortcutInfo info) { - mMainExecutor.execute(() -> mController.expandStackAndSelectBubble(info)); + mMainExecutor.execute(() -> mController + .expandStackAndSelectBubble(info, /* bubbleBarLocation = */ null)); } @Override public void showAppBubble(Intent intent, UserHandle user) { - mMainExecutor.execute(() -> mController.expandStackAndSelectBubble(intent, - user, /* bubbleBarLocation = */ null)); + mMainExecutor.execute(() -> mController.expandStackAndSelectBubble(intent, user)); } @Override @@ -2983,9 +3041,10 @@ public class BubbleController implements ConfigurationChangeListener, @Override public void expandStackAndSelectBubble(ShortcutInfo info) { - mMainExecutor.execute(() -> { - BubbleController.this.expandStackAndSelectBubble(info); - }); + mMainExecutor.execute(() -> + BubbleController.this + .expandStackAndSelectBubble(info, /* bubbleBarLocation = */ null) + ); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index 96d0f6d5654e..abcdb7e70cec 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -43,6 +43,7 @@ import com.android.wm.shell.R; import com.android.wm.shell.bubbles.Bubbles.DismissReason; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import com.android.wm.shell.shared.bubbles.BubbleBarUpdate; import com.android.wm.shell.shared.bubbles.RemovedBubble; @@ -91,6 +92,8 @@ public class BubbleData { @Nullable Bubble suppressedBubble; @Nullable Bubble unsuppressedBubble; @Nullable String suppressedSummaryGroup; + @Nullable + BubbleBarLocation mBubbleBarLocation; // Pair with Bubble and @DismissReason Integer final List<Pair<Bubble, Integer>> removedBubbles = new ArrayList<>(); @@ -116,6 +119,7 @@ public class BubbleData { || unsuppressedBubble != null || suppressedSummaryChanged || suppressedSummaryGroup != null + || mBubbleBarLocation != null || showOverflowChanged; } @@ -169,6 +173,7 @@ public class BubbleData { } bubbleBarUpdate.showOverflowChanged = showOverflowChanged; bubbleBarUpdate.showOverflow = !overflowBubbles.isEmpty(); + bubbleBarUpdate.bubbleBarLocation = mBubbleBarLocation; return bubbleBarUpdate; } @@ -396,8 +401,23 @@ public class BubbleData { * {@link #setExpanded(boolean)} immediately after, which will generate 2 separate updates. */ public void setSelectedBubbleAndExpandStack(BubbleViewProvider bubble) { + setSelectedBubbleAndExpandStack(bubble, /* bubbleBarLocation = */ null); + } + + /** + * Sets the selected bubble and expands it. Also updates bubble bar location if the + * bubbleBarLocation is not {@code null} + * + * <p>This dispatches a single state update for 3 changes and should be used instead of + * calling {@link BubbleController#setBubbleBarLocation(BubbleBarLocation, int)} followed by + * {@link #setSelectedBubbleAndExpandStack(BubbleViewProvider)} immediately after, which will + * generate 2 separate updates. + */ + public void setSelectedBubbleAndExpandStack(BubbleViewProvider bubble, + @Nullable BubbleBarLocation bubbleBarLocation) { setSelectedBubbleInternal(bubble); setExpandedInternal(true); + mStateChange.mBubbleBarLocation = bubbleBarLocation; dispatchPendingChanges(); } @@ -471,6 +491,16 @@ public class BubbleData { return bubbleToReturn; } + Bubble getOrCreateBubble(PendingIntent pendingIntent, UserHandle user) { + String bubbleKey = Bubble.getAppBubbleKeyForApp(pendingIntent.getCreatorPackage(), user); + Bubble bubbleToReturn = findAndRemoveBubbleFromOverflow(bubbleKey); + if (bubbleToReturn == null) { + bubbleToReturn = Bubble.createAppBubble(pendingIntent, user, mMainExecutor, + mBgExecutor); + } + return bubbleToReturn; + } + Bubble getOrCreateBubble(TaskInfo taskInfo) { UserHandle user = UserHandle.of(mCurrentUserId); String bubbleKey = Bubble.getAppBubbleKeyForTask(taskInfo); @@ -503,13 +533,25 @@ public class BubbleData { } /** + * Calls {@link #notificationEntryUpdated(Bubble, boolean, boolean, BubbleBarLocation)} passing + * {@code null} for bubbleBarLocation. + * + * @see #notificationEntryUpdated(Bubble, boolean, boolean, BubbleBarLocation) + */ + void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) { + notificationEntryUpdated(bubble, suppressFlyout, showInShade, /* bubbleBarLocation = */ + null); + } + + /** * When this method is called it is expected that all info in the bubble has completed loading. * @see Bubble#inflate(BubbleViewInfoTask.Callback, Context, BubbleExpandedViewManager, * BubbleTaskViewFactory, BubblePositioner, BubbleLogger, BubbleStackView, * com.android.wm.shell.bubbles.bar.BubbleBarLayerView, * com.android.launcher3.icons.BubbleIconFactory, boolean) */ - void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) { + void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade, + @Nullable BubbleBarLocation bubbleBarLocation) { mPendingBubbles.remove(bubble.getKey()); // No longer pending once we're here Bubble prevBubble = getBubbleInStackWithKey(bubble.getKey()); suppressFlyout |= !bubble.isTextChanged(); @@ -557,6 +599,7 @@ public class BubbleData { doSuppress(bubble); } } + mStateChange.mBubbleBarLocation = bubbleBarLocation; dispatchPendingChanges(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java index 4a0eee861d21..e47ac61a53dd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java @@ -117,15 +117,24 @@ public class BubbleTaskViewHelper { Context context = mContext.createContextAsUser( mBubble.getUser(), Context.CONTEXT_RESTRICTED); - PendingIntent pi = PendingIntent.getActivity( - context, - /* requestCode= */ 0, - mBubble.getIntent() - .addFlags(FLAG_ACTIVITY_MULTIPLE_TASK), - PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT, - /* options= */ null); - mTaskView.startActivity(pi, /* fillInIntent= */ null, options, - launchBounds); + Intent fillInIntent = null; + //first try get pending intent from the bubble + PendingIntent pi = mBubble.getPendingIntent(); + if (pi == null) { + // if null - create new one + pi = PendingIntent.getActivity( + context, + /* requestCode= */ 0, + mBubble.getIntent() + .addFlags(FLAG_ACTIVITY_MULTIPLE_TASK), + PendingIntent.FLAG_IMMUTABLE + | PendingIntent.FLAG_UPDATE_CURRENT, + /* options= */ null); + } else { + fillInIntent = new Intent(pi.getIntent()); + fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + } + mTaskView.startActivity(pi, fillInIntent, options, launchBounds); } else if (isShortcutBubble) { options.setLaunchedFromBubble(true); options.setApplyActivityFlagsForBubbles(true); 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 index afe5c87604d9..3ff80b5ab8ac 100644 --- 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 @@ -18,7 +18,6 @@ package com.android.wm.shell.bubbles.bar import android.content.Intent import android.graphics.Rect -import android.os.UserHandle import com.android.wm.shell.shared.bubbles.BubbleBarLocation /** Controller that takes care of the bubble bar drag events. */ @@ -31,11 +30,7 @@ interface BubbleBarDragListener { fun onItemDraggedOutsideBubbleBarDropZone() /** Called when the drop event happens over the bubble bar drop zone. */ - fun onItemDroppedOverBubbleBarDragZone( - location: BubbleBarLocation, - intent: Intent, - userHandle: UserHandle - ) + fun onItemDroppedOverBubbleBarDragZone(location: BubbleBarLocation, itemIntent: Intent) /** * Returns mapping of the bubble bar locations to the corresponding diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java index 78c6cf377d8e..6798a88a6da7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java @@ -180,6 +180,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView // Ideally this would be package private, but we have to set this in a fake for test and we // don't yet have dagger set up for tests, so have to set manually + @VisibleForTesting @Inject public BubbleLogger bubbleLogger; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index aa42de67152a..e3b0872df593 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -524,8 +524,8 @@ public class BubbleBarLayerView extends FrameLayout * Skips logging if it is {@link BubbleOverflow}. */ private void logBubbleEvent(BubbleLogger.Event event) { - if (mExpandedBubble != null && mExpandedBubble instanceof Bubble bubble) { - mBubbleLogger.log(bubble, event); + if (mExpandedBubble != null && mExpandedBubble instanceof Bubble) { + mBubbleLogger.log((Bubble) mExpandedBubble, event); } } 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 b2b99d648bf4..b6012378e4d4 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 @@ -914,12 +914,15 @@ public abstract class WMShellModule { Context context, Transitions transitions, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + @DynamicOverride DesktopUserRepositories desktopUserRepositories, InteractionJankMonitor interactionJankMonitor) { return ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITIONS_BUGFIX.isTrue() ? new SpringDragToDesktopTransitionHandler( - context, transitions, rootTaskDisplayAreaOrganizer, interactionJankMonitor) + context, transitions, rootTaskDisplayAreaOrganizer, desktopUserRepositories, + interactionJankMonitor) : new DefaultDragToDesktopTransitionHandler( - context, transitions, rootTaskDisplayAreaOrganizer, interactionJankMonitor); + context, transitions, rootTaskDisplayAreaOrganizer, desktopUserRepositories, + interactionJankMonitor); } @WMSingleton 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 3c7780711a14..531304d6922a 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 @@ -343,10 +343,22 @@ class DesktopTasksController( DesktopModeFlags.INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC .isTrue() && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() ) { + logV( + "isDesktopModeShowing: hasVisibleTasks=%s hasTopTransparentFullscreenTask=%s hasMinimizedPip=%s", + hasVisibleTasks, + hasTopTransparentFullscreenTask, + hasMinimizedPip, + ) return hasVisibleTasks || hasTopTransparentFullscreenTask || hasMinimizedPip } else if (Flags.enableDesktopWindowingPip()) { + logV( + "isDesktopModeShowing: hasVisibleTasks=%s hasMinimizedPip=%s", + hasVisibleTasks, + hasMinimizedPip, + ) return hasVisibleTasks || hasMinimizedPip } + logV("isDesktopModeShowing: hasVisibleTasks=%s", hasVisibleTasks) return hasVisibleTasks } @@ -1647,11 +1659,16 @@ class DesktopTasksController( private fun addWallpaperActivity(displayId: Int, wct: WindowContainerTransaction) { logV("addWallpaperActivity") if (ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER.isTrue()) { + + // If the wallpaper activity for this display already exists, let's reorder it to top. + val wallpaperActivityToken = desktopWallpaperActivityTokenProvider.getToken(displayId) + if (wallpaperActivityToken != null) { + wct.reorder(wallpaperActivityToken, /* onTop= */ true) + return + } + val intent = Intent(context, DesktopWallpaperActivity::class.java) - if ( - desktopWallpaperActivityTokenProvider.getToken(displayId) == null && - Flags.enablePerDisplayDesktopWallpaperActivity() - ) { + if (Flags.enablePerDisplayDesktopWallpaperActivity()) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK) } @@ -3074,6 +3091,7 @@ class DesktopTasksController( ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS pendingIntentLaunchFlags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK + splashScreenStyle = SPLASH_SCREEN_STYLE_ICON } if (windowingMode == WINDOWING_MODE_FULLSCREEN) { dragAndDropFullscreenCookie = Binder() @@ -3082,7 +3100,12 @@ class DesktopTasksController( val wct = WindowContainerTransaction() wct.sendPendingIntent(launchIntent, null, opts.toBundle()) if (windowingMode == WINDOWING_MODE_FREEFORM) { - desktopModeDragAndDropTransitionHandler.handleDropEvent(wct) + if (DesktopModeFlags.ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX.isTrue()) { + // TODO b/376389593: Use a custom tab tearing transition/animation + startLaunchTransition(TRANSIT_OPEN, wct, launchingTaskId = null) + } else { + desktopModeDragAndDropTransitionHandler.handleDropEvent(wct) + } } else { transitions.startTransition(TRANSIT_OPEN, wct, null) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt index a5ba6612bb1a..c10752d36bf9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt @@ -90,6 +90,11 @@ class DesktopUserRepositories( return desktopRepoByUserId.getOrCreate(profileId) } + fun getUserIdForProfile(profileId: Int): Int { + if (userIdToProfileIdsMap[userId]?.contains(profileId) == true) return userId + else return profileId + } + /** Dumps [DesktopRepository] for each user. */ fun dump(pw: PrintWriter, prefix: String) { desktopRepoByUserId.forEach { key, value -> 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 2ac76f319d32..8194d3cab445 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 @@ -70,6 +70,7 @@ sealed class DragToDesktopTransitionHandler( private val context: Context, private val transitions: Transitions, private val taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + private val desktopUserRepositories: DesktopUserRepositories, protected val interactionJankMonitor: InteractionJankMonitor, protected val transactionSupplier: Supplier<SurfaceControl.Transaction>, ) : TransitionHandler { @@ -127,15 +128,18 @@ sealed class DragToDesktopTransitionHandler( pendingIntentCreatorBackgroundActivityStartMode = ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED } - val taskUser = UserHandle.of(taskInfo.userId) + // If we are launching home for a profile of a user, just use the [userId] of that user + // instead of the [profileId] to create the context. + val userToLaunchWith = + UserHandle.of(desktopUserRepositories.getUserIdForProfile(taskInfo.userId)) val pendingIntent = PendingIntent.getActivityAsUser( - context.createContextAsUser(taskUser, /* flags= */ 0), + context.createContextAsUser(userToLaunchWith, /* flags= */ 0), /* requestCode= */ 0, launchHomeIntent, FLAG_MUTABLE or FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or FILL_IN_COMPONENT, options.toBundle(), - taskUser, + userToLaunchWith, ) val wct = WindowContainerTransaction() // The app that is being dragged into desktop mode might cause new transitions, make this @@ -881,6 +885,7 @@ constructor( context: Context, transitions: Transitions, taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + desktopUserRepositories: DesktopUserRepositories, interactionJankMonitor: InteractionJankMonitor, transactionSupplier: Supplier<SurfaceControl.Transaction> = Supplier { SurfaceControl.Transaction() @@ -890,6 +895,7 @@ constructor( context, transitions, taskDisplayAreaOrganizer, + desktopUserRepositories, interactionJankMonitor, transactionSupplier, ) { @@ -917,6 +923,7 @@ constructor( context: Context, transitions: Transitions, taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + desktopUserRepositories: DesktopUserRepositories, interactionJankMonitor: InteractionJankMonitor, transactionSupplier: Supplier<SurfaceControl.Transaction> = Supplier { SurfaceControl.Transaction() @@ -926,6 +933,7 @@ constructor( context, transitions, taskDisplayAreaOrganizer, + desktopUserRepositories, interactionJankMonitor, transactionSupplier, ) { 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 2571e0e36cd9..b3c1a92f5e1d 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 @@ -47,7 +47,6 @@ import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; import android.graphics.drawable.Drawable; -import android.os.UserHandle; import android.view.DragEvent; import android.view.SurfaceControl; import android.view.View; @@ -70,6 +69,7 @@ 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.BubbleAnythingFlagHelper; import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import com.android.wm.shell.splitscreen.SplitScreenController; @@ -627,8 +627,7 @@ public class DragLayout extends LinearLayout @Nullable private BubbleBarLocation getBubbleBarLocation(int x, int y) { Intent appData = mSession.appData; - if (appData == null || appData.getExtra(Intent.EXTRA_INTENT) == null - || appData.getExtra(Intent.EXTRA_USER) == null) { + if (appData == null) { // there is no app data, so drop event over the bubble bar can not be handled return null; } @@ -686,11 +685,10 @@ public class DragLayout extends LinearLayout // Process the drop exclusive by DropTarget OR by the BubbleBar if (mCurrentTarget != null) { mPolicy.onDropped(mCurrentTarget, hideTaskToken); - } else if (appData != null && mCurrentBubbleBarTarget != null) { - Intent appIntent = (Intent) appData.getExtra(Intent.EXTRA_INTENT); - UserHandle user = (UserHandle) appData.getExtra(Intent.EXTRA_USER); + } else if (appData != null && mCurrentBubbleBarTarget != null + && BubbleAnythingFlagHelper.enableCreateAnyBubble()) { mBubbleBarDragListener.onItemDroppedOverBubbleBarDragZone(mCurrentBubbleBarTarget, - appIntent, user); + appData); } // Start animating the drop UI out with the drag surface diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java index d666126b91ba..c0a0f469add4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java @@ -22,6 +22,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.service.dreams.Flags.dismissDreamOnKeyguardDismiss; import static android.view.WindowManager.KEYGUARD_VISIBILITY_TRANSIT_FLAGS; +import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED; @@ -200,7 +201,8 @@ public class KeyguardTransitionHandler transition, info, startTransaction, finishTransaction, finishCallback); } - if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_APPEARING) != 0) { + if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_APPEARING) != 0 + || (info.getFlags() & TRANSIT_FLAG_AOD_APPEARING) != 0) { return startAnimation(mAppearTransition, "appearing", transition, info, startTransaction, finishTransaction, finishCallback); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java index b4cf8905d02e..88ac865c24b9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java @@ -26,6 +26,7 @@ import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.hardware.HardwareBuffer; import android.util.TypedValue; import android.view.SurfaceControl; @@ -39,7 +40,6 @@ public final class PipAppIconOverlay extends PipContentOverlay { private final Context mContext; private final int mAppIconSizePx; - private final Rect mAppBounds; private final int mOverlayHalfSize; private final Matrix mTmpTransform = new Matrix(); private final float[] mTmpFloat9 = new float[9]; @@ -56,10 +56,6 @@ public final class PipAppIconOverlay extends PipContentOverlay { final int overlaySize = getOverlaySize(appBounds, destinationBounds); mOverlayHalfSize = overlaySize >> 1; - // When the activity is in the secondary split, make sure the scaling center is not - // offset. - mAppBounds = new Rect(0, 0, appBounds.width(), appBounds.height()); - mBitmap = Bitmap.createBitmap(overlaySize, overlaySize, Bitmap.Config.ARGB_8888); prepareAppIconOverlay(appIcon); mLeash = new SurfaceControl.Builder() @@ -85,12 +81,17 @@ public final class PipAppIconOverlay extends PipContentOverlay { @Override public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) { + final HardwareBuffer buffer = mBitmap.getHardwareBuffer(); tx.show(mLeash); tx.setLayer(mLeash, Integer.MAX_VALUE); - tx.setBuffer(mLeash, mBitmap.getHardwareBuffer()); + tx.setBuffer(mLeash, buffer); tx.setAlpha(mLeash, 0f); tx.reparent(mLeash, parentLeash); tx.apply(); + // Cleanup the bitmap and buffer after setting up the leash + mBitmap.recycle(); + mBitmap = null; + buffer.close(); } @Override @@ -108,16 +109,6 @@ public final class PipAppIconOverlay extends PipContentOverlay { .setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2); } - - - @Override - public void detach(SurfaceControl.Transaction tx) { - super.detach(tx); - if (mBitmap != null && !mBitmap.isRecycled()) { - mBitmap.recycle(); - } - } - private void prepareAppIconOverlay(Drawable appIcon) { final Canvas canvas = new Canvas(); canvas.setBitmap(mBitmap); @@ -139,6 +130,8 @@ public final class PipAppIconOverlay extends PipContentOverlay { mOverlayHalfSize + mAppIconSizePx / 2); appIcon.setBounds(appIconBounds); appIcon.draw(canvas); + Bitmap oldBitmap = mBitmap; mBitmap = mBitmap.copy(Bitmap.Config.HARDWARE, false /* mutable */); + oldBitmap.recycle(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index bb9b479524e5..a57b4b948b42 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -72,7 +72,6 @@ import com.android.wm.shell.pip2.animation.PipAlphaAnimator; import com.android.wm.shell.pip2.animation.PipEnterAnimator; import com.android.wm.shell.pip2.animation.PipExpandAnimator; import com.android.wm.shell.shared.TransitionUtil; -import com.android.wm.shell.shared.pip.PipContentOverlay; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; @@ -422,7 +421,7 @@ public class PipTransition extends PipTransitionController implements final Rect destinationBounds = pipChange.getEndAbsBounds(); final SurfaceControl swipePipToHomeOverlay = mPipTransitionState.getSwipePipToHomeOverlay(); if (swipePipToHomeOverlay != null) { - final int overlaySize = PipContentOverlay.PipAppIconOverlay.getOverlaySize( + final int overlaySize = PipAppIconOverlay.getOverlaySize( mPipTransitionState.getSwipePipToHomeAppBounds(), destinationBounds); // It is possible we reparent the PIP activity to a new PIP task (in multi-activity // apps), so we should also reparent the overlay to the final PIP task. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index 8ad2e1d3c7c9..7751741ae082 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -29,6 +29,7 @@ import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_SLEEP; import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.window.DesktopModeFlags.ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX; import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; @@ -46,6 +47,7 @@ import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.IApplicationThread; import android.app.PendingIntent; +import android.content.Context; import android.content.Intent; import android.graphics.Color; import android.graphics.Rect; @@ -73,6 +75,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.IResultReceiver; import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.Flags; +import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipUtils; @@ -317,7 +320,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, "RecentsTransitionHandler.mergeAnimation: no controller found"); return; } - controller.merge(info, startT, mergeTarget, finishCallback); + controller.merge(info, startT, finishT, mergeTarget, finishCallback); } @Override @@ -912,7 +915,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, * before any unhandled transitions. */ @SuppressLint("NewApi") - void merge(TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget, + void merge(TransitionInfo info, SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT, IBinder mergeTarget, Transitions.TransitionFinishCallback finishCallback) { if (mFinishCB == null) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, @@ -1072,8 +1076,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, Slog.e(TAG, "Returning to recents without closing any opening tasks."); } // Setup may hide it initially since it doesn't know that overview was still active. - t.show(recentsOpening.getLeash()); - t.setAlpha(recentsOpening.getLeash(), 1.f); + startT.show(recentsOpening.getLeash()); + startT.setAlpha(recentsOpening.getLeash(), 1.f); mState = STATE_NORMAL; } boolean didMergeThings = false; @@ -1142,31 +1146,31 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, mOpeningTasks.add(pausingTask); // Setup hides opening tasks initially, so make it visible again (since we // are already showing it). - t.show(change.getLeash()); - t.setAlpha(change.getLeash(), 1.f); + startT.show(change.getLeash()); + startT.setAlpha(change.getLeash(), 1.f); } else if (isLeaf) { // We are receiving new opening leaf tasks, so convert to onTasksAppeared. final RemoteAnimationTarget target = TransitionUtil.newTarget( - change, layer, info, t, mLeashMap); + change, layer, info, startT, mLeashMap); appearedTargets[nextTargetIdx++] = target; // reparent into the original `mInfo` since that's where we are animating. final TransitionInfo.Root root = TransitionUtil.getRootFor(change, mInfo); final boolean wasClosing = closingIdx >= 0; - t.reparent(target.leash, root.getLeash()); - t.setPosition(target.leash, + startT.reparent(target.leash, root.getLeash()); + startT.setPosition(target.leash, change.getStartAbsBounds().left - root.getOffset().x, change.getStartAbsBounds().top - root.getOffset().y); - t.setLayer(target.leash, layer); + startT.setLayer(target.leash, layer); if (wasClosing) { // App was previously visible and is closing - t.show(target.leash); - t.setAlpha(target.leash, 1f); + startT.show(target.leash); + startT.setAlpha(target.leash, 1f); // Also override the task alpha as it was set earlier when dispatching // the transition and setting up the leash to hide the - t.setAlpha(change.getLeash(), 1f); + startT.setAlpha(change.getLeash(), 1f); } else { // Hide the animation leash, let the listener show it - t.hide(target.leash); + startT.hide(target.leash); } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, " opening new leaf taskId=%d wasClosing=%b", @@ -1175,10 +1179,10 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, } else { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, " opening new taskId=%d", change.getTaskInfo().taskId); - t.setLayer(change.getLeash(), layer); + startT.setLayer(change.getLeash(), layer); // Setup hides opening tasks initially, so make it visible since recents // is only animating the leafs. - t.show(change.getLeash()); + startT.show(change.getLeash()); mOpeningTasks.add(new TaskState(change, null)); } } @@ -1194,7 +1198,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, // Activity only transition, so consume the merge as it doesn't affect the rest of // recents. Slog.d(TAG, "Got an activity only transition during recents, so apply directly"); - mergeActivityOnly(info, t); + mergeActivityOnly(info, startT); } else if (!didMergeThings) { // Didn't recognize anything in incoming transition so don't merge it. Slog.w(TAG, "Don't know how to merge this transition, foundRecentsClosing=" @@ -1206,7 +1210,10 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, return; } // At this point, we are accepting the merge. - t.apply(); + startT.apply(); + // Since we're accepting the merge, update the finish transaction so that changes via + // that transaction will be applied on top of those of the merged transitions + mFinishTransaction = finishT; // not using the incoming anim-only surfaces info.releaseAnimSurfaces(); if (appearedTargets != null) { @@ -1349,6 +1356,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, wct.reorder(mPausingTasks.get(i).mToken, true /* onTop */); t.show(mPausingTasks.get(i).mTaskSurface); } + setCornerRadiusForFreeformTasks( + mRecentTasksController.getContext(), t, mPausingTasks); if (!mKeyguardLocked && mRecentsTask != null) { wct.restoreTransientOrder(mRecentsTask); } @@ -1386,6 +1395,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, for (int i = 0; i < mOpeningTasks.size(); ++i) { t.show(mOpeningTasks.get(i).mTaskSurface); } + setCornerRadiusForFreeformTasks( + mRecentTasksController.getContext(), t, mOpeningTasks); for (int i = 0; i < mPausingTasks.size(); ++i) { cleanUpPausingOrClosingTask(mPausingTasks.get(i), wct, t, sendUserLeaveHint); } @@ -1446,6 +1457,11 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, wct.clear(); if (Flags.enableRecentsBookendTransition()) { + // Notify the mixers of the pending finish + for (int i = 0; i < mMixers.size(); ++i) { + mMixers.get(i).handleFinishRecents(returningToApp, wct, t); + } + // In this case, we've already started the PIP transition, so we can // clean up immediately mPendingRunnerFinishCb = runnerFinishCb; @@ -1505,6 +1521,27 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, } } + private static void setCornerRadiusForFreeformTasks( + Context context, + SurfaceControl.Transaction t, + ArrayList<TaskState> tasks) { + if (!ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX.isTrue()) { + return; + } + int cornerRadius = getCornerRadius(context); + for (int i = 0; i < tasks.size(); ++i) { + TaskState task = tasks.get(i); + if (task.mTaskInfo != null && task.mTaskInfo.isFreeform()) { + t.setCornerRadius(task.mTaskSurface, cornerRadius); + } + } + } + + private static int getCornerRadius(Context context) { + return context.getResources().getDimensionPixelSize( + R.dimen.desktop_windowing_freeform_rounded_corner_radius); + } + private boolean allAppsAreTranslucent(ArrayList<TaskState> tasks) { if (tasks == null) { return false; 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 aff21cbe0ae6..a799b7f2580e 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 @@ -1675,8 +1675,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, void prepareExitSplitScreen(@StageType int stageToTop, @NonNull WindowContainerTransaction wct, @ExitReason int exitReason) { if (!isSplitActive()) return; - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareExitSplitScreen: stageToTop=%s", - stageTypeToString(stageToTop)); + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareExitSplitScreen: stageToTop=%s reason=%s", + stageTypeToString(stageToTop), exitReasonToString(exitReason)); if (enableFlexibleSplit()) { mStageOrderOperator.getActiveStages().stream() .filter(stage -> stage.getId() != stageToTop) @@ -2859,14 +2859,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, prepareExitSplitScreen(dismissTop, out, EXIT_REASON_APP_FINISHED); mSplitTransitions.setDismissTransition(transition, dismissTop, EXIT_REASON_APP_FINISHED); - } else if (isOpening && !mPausingTasks.isEmpty()) { - // One of the splitting task is opening while animating the split pair in - // recents, which means to dismiss the split pair to this task. - int dismissTop = getStageType(stage) == STAGE_TYPE_MAIN - ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE; - prepareExitSplitScreen(dismissTop, out, EXIT_REASON_APP_FINISHED); - mSplitTransitions.setDismissTransition(transition, dismissTop, - EXIT_REASON_APP_FINISHED); } else if (!isSplitScreenVisible() && isOpening) { // If split is running in the background and the trigger task is appearing into // split, prepare to enter split screen. @@ -3395,12 +3387,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, TransitionInfo.Change sideChild = null; StageTaskListener firstAppStage = null; StageTaskListener secondAppStage = null; + boolean foundPausingTask = false; final WindowContainerTransaction evictWct = new WindowContainerTransaction(); for (int iC = 0; iC < info.getChanges().size(); ++iC) { final TransitionInfo.Change change = info.getChanges().get(iC); final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); if (taskInfo == null || !taskInfo.hasParentTask()) continue; if (mPausingTasks.contains(taskInfo.taskId)) { + foundPausingTask = true; continue; } StageTaskListener stage = getStageOfTask(taskInfo); @@ -3443,9 +3437,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, prepareExitSplitScreen(dismissTop, cancelWct, EXIT_REASON_UNKNOWN); logExit(EXIT_REASON_UNKNOWN); }); - Log.w(TAG, splitFailureMessage("startPendingEnterAnimation", - "launched 2 tasks in split, but didn't receive " - + "2 tasks in transition. Possibly one of them failed to launch")); + Log.w(TAG, splitFailureMessage("startPendingEnterAnimation", "launched 2 tasks in " + + "split, but didn't receive 2 tasks in transition. Possibly one of them " + + "failed to launch (foundPausingTask=" + foundPausingTask + ")")); if (mRecentTasks.isPresent() && mainChild != null) { mRecentTasks.get().removeSplitPair(mainChild.getTaskInfo().taskId); } @@ -3800,6 +3794,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, /** Call this when the recents animation canceled during split-screen. */ public void onRecentsInSplitAnimationCanceled() { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRecentsInSplitAnimationCanceled"); mPausingTasks.clear(); setSplitsVisible(false); @@ -3809,31 +3804,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mTaskOrganizer.applyTransaction(wct); } - public void onRecentsInSplitAnimationFinishing(boolean returnToApp, - @NonNull WindowContainerTransaction finishWct, - @NonNull SurfaceControl.Transaction finishT) { - if (!Flags.enableRecentsBookendTransition()) { - // The non-bookend recents transition case will be handled by - // RecentsMixedTransition wrapping the finish callback and calling - // onRecentsInSplitAnimationFinish() - return; - } - - onRecentsInSplitAnimationFinishInner(returnToApp, finishWct, finishT); - } - - /** Call this when the recents animation during split-screen finishes. */ - public void onRecentsInSplitAnimationFinish(@NonNull WindowContainerTransaction finishWct, - @NonNull SurfaceControl.Transaction finishT) { - if (Flags.enableRecentsBookendTransition()) { - // The bookend recents transition case will be handled by - // onRecentsInSplitAnimationFinishing above - return; - } - - // Check if the recent transition is finished by returning to the current - // split, so we can restore the divider bar. - boolean returnToApp = false; + /** + * Returns whether the given WCT is reordering any of the split tasks to top. + */ + public boolean wctIsReorderingSplitToTop(@NonNull WindowContainerTransaction finishWct) { for (int i = 0; i < finishWct.getHierarchyOps().size(); ++i) { final WindowContainerTransaction.HierarchyOp op = finishWct.getHierarchyOps().get(i); @@ -3848,14 +3822,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop() && anyStageContainsContainer) { - returnToApp = true; + return true; } } - onRecentsInSplitAnimationFinishInner(returnToApp, finishWct, finishT); + return false; } - /** Call this when the recents animation during split-screen finishes. */ - public void onRecentsInSplitAnimationFinishInner(boolean returnToApp, + /** Called when the recents animation during split-screen finishes. */ + public void onRecentsInSplitAnimationFinishing(boolean returnToApp, @NonNull WindowContainerTransaction finishWct, @NonNull SurfaceControl.Transaction finishT) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRecentsInSplitAnimationFinish: returnToApp=%b", diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index 0689205a1110..a5a5fd73d5c0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -39,9 +39,12 @@ import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE; import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS; import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED; import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE; +import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_RELAUNCH; import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_OWNER_THUMBNAIL; import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_WORK_THUMBNAIL; import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS; @@ -55,6 +58,7 @@ import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; +import static com.android.internal.jank.Cuj.CUJ_DEFAULT_TASK_TO_TASK_ANIMATION; import static com.android.internal.policy.TransitionAnimation.DEFAULT_APP_TRANSITION_DURATION; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CHANGE; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE; @@ -101,6 +105,7 @@ import android.window.WindowContainerTransaction; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.policy.ScreenDecorationsUtils; import com.android.internal.policy.TransitionAnimation; import com.android.internal.protolog.ProtoLog; @@ -144,6 +149,9 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { private Drawable mEnterpriseThumbnailDrawable; + static final InteractionJankMonitor sInteractionJankMonitor = + InteractionJankMonitor.getInstance(); + private BroadcastReceiver mEnterpriseResourceUpdatedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -321,8 +329,17 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final ArrayList<Animator> animations = new ArrayList<>(); mAnimations.put(transition, animations); + final boolean isTaskTransition = isTaskTransition(info); + if (isTaskTransition) { + sInteractionJankMonitor.begin(info.getRoot(0).getLeash(), mContext, + mMainHandler, CUJ_DEFAULT_TASK_TO_TASK_ANIMATION); + } + final Runnable onAnimFinish = () -> { if (!animations.isEmpty()) return; + if (isTaskTransition) { + sInteractionJankMonitor.end(CUJ_DEFAULT_TASK_TO_TASK_ANIMATION); + } mAnimations.remove(transition); finishCallback.onTransitionFinished(null /* wct */); }; @@ -678,6 +695,30 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } /** + * A task transition is defined as a transition where there is exaclty one open/to_front task + * and one close/to_back task. Nothing else is allowed to be included in the transition + */ + public static boolean isTaskTransition(@NonNull TransitionInfo info) { + if (info.getChanges().size() != 2) { + return false; + } + boolean hasOpeningTask = false; + boolean hasClosingTask = false; + + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (change.getTaskInfo() == null) { + // A non-task is in the transition + return false; + } + int mode = change.getMode(); + hasOpeningTask |= mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT; + hasClosingTask |= mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK; + } + return hasOpeningTask && hasClosingTask; + } + + /** * Does `info` only contain translucent visibility changes (CHANGEs are ignored). We select * different animations and z-orders for these */ @@ -986,4 +1027,10 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { || animType == ANIM_CLIP_REVEAL || animType == ANIM_OPEN_CROSS_PROFILE_APPS || animType == ANIM_FROM_STYLE; } + + @Override + public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted, + @Nullable SurfaceControl.Transaction finishTransaction) { + sInteractionJankMonitor.cancel(CUJ_DEFAULT_TASK_TO_TASK_ANIMATION); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java index f40dc8ad93b5..1e926c57ca61 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java @@ -159,9 +159,17 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition { // If pair-to-pair switching, the post-recents clean-up isn't needed. wct = wct != null ? wct : new WindowContainerTransaction(); if (mAnimType != ANIM_TYPE_PAIR_TO_PAIR) { - // TODO(b/346588978): Only called if !enableRecentsBookendTransition(), can remove - // once that rolls out - mSplitHandler.onRecentsInSplitAnimationFinish(wct, finishTransaction); + // We've dispatched to the mLeftoversHandler to handle the rest of the transition + // and called onRecentsInSplitAnimationStart(), but if the recents handler is not + // actually handling the transition, then onRecentsInSplitAnimationFinishing() + // won't actually get called by the recents handler. In such cases, we still need + // to clean up after the changes from the start call. + boolean splitNotifiedByRecents = mRecentsHandler == mLeftoversHandler; + if (!splitNotifiedByRecents) { + mSplitHandler.onRecentsInSplitAnimationFinishing( + mSplitHandler.wctIsReorderingSplitToTop(wct), + wct, finishTransaction); + } } else { // notify pair-to-pair recents animation finish mSplitHandler.onRecentsPairToPairAnimationFinish(wct); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt index 1d9564948772..a17bcb39f1a0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt @@ -35,10 +35,8 @@ import android.view.SurfaceControl import android.view.View import android.view.WindowInsets.Type.systemBars import android.view.WindowManager -import android.widget.Button import android.widget.ImageButton import android.widget.ImageView -import android.widget.TextView import android.window.DesktopModeFlags import android.window.SurfaceSyncGroup import androidx.annotation.StringRes @@ -473,7 +471,7 @@ class HandleMenu( @VisibleForTesting val appIconView = appInfoPill.requireViewById<ImageView>(R.id.application_icon) @VisibleForTesting - val appNameView = appInfoPill.requireViewById<TextView>(R.id.application_name) + val appNameView = appInfoPill.requireViewById<MarqueedTextView>(R.id.application_name) // Windowing Pill. private val windowingPill = rootView.requireViewById<View>(R.id.windowing_pill) @@ -486,17 +484,17 @@ class HandleMenu( // More Actions Pill. private val moreActionsPill = rootView.requireViewById<View>(R.id.more_actions_pill) - private val screenshotBtn = moreActionsPill.requireViewById<Button>(R.id.screenshot_button) - private val newWindowBtn = moreActionsPill.requireViewById<Button>(R.id.new_window_button) + private val screenshotBtn = moreActionsPill.requireViewById<View>(R.id.screenshot_button) + private val newWindowBtn = moreActionsPill.requireViewById<View>(R.id.new_window_button) private val manageWindowBtn = moreActionsPill - .requireViewById<Button>(R.id.manage_windows_button) + .requireViewById<View>(R.id.manage_windows_button) private val changeAspectRatioBtn = moreActionsPill - .requireViewById<Button>(R.id.change_aspect_ratio_button) + .requireViewById<View>(R.id.change_aspect_ratio_button) // Open in Browser/App Pill. private val openInAppOrBrowserPill = rootView.requireViewById<View>( R.id.open_in_app_or_browser_pill) - private val openInAppOrBrowserBtn = openInAppOrBrowserPill.requireViewById<Button>( + private val openInAppOrBrowserBtn = openInAppOrBrowserPill.requireViewById<View>( R.id.open_in_app_or_browser_button) private val openByDefaultBtn = openInAppOrBrowserPill.requireViewById<ImageButton>( R.id.open_by_default_button) @@ -540,17 +538,35 @@ class HandleMenu( return@setOnTouchListener true } - with(context.resources) { - // Update a11y read out to say "double tap to enter desktop windowing mode" + with(context) { + // Update a11y announcement out to say "double tap to enter Fullscreen" + ViewCompat.replaceAccessibilityAction( + fullscreenBtn, ACTION_CLICK, + getString( + R.string.app_handle_menu_accessibility_announce, + getString(R.string.fullscreen_text) + ), + null, + ) + + // Update a11y announcement out to say "double tap to enter Desktop View" ViewCompat.replaceAccessibilityAction( desktopBtn, ACTION_CLICK, - getString(R.string.app_handle_menu_talkback_desktop_mode_button_text), null + getString( + R.string.app_handle_menu_accessibility_announce, + getString(R.string.desktop_text) + ), + null, ) - // Update a11y read out to say "double tap to enter split screen mode" + // Update a11y announcement to say "double tap to enter Split Screen" ViewCompat.replaceAccessibilityAction( splitscreenBtn, ACTION_CLICK, - getString(R.string.app_handle_menu_talkback_split_screen_mode_button_text), null + getString( + R.string.app_handle_menu_accessibility_announce, + getString(R.string.split_screen_text) + ), + null, ) } } @@ -658,6 +674,7 @@ class HandleMenu( this.taskInfo = this@HandleMenuView.taskInfo } appNameView.setTextColor(style.textColor) + appNameView.startMarquee() } private fun bindWindowingPill(style: MenuStyle) { @@ -693,11 +710,15 @@ class HandleMenu( ).forEach { val button = it.first val shouldShow = it.second - button.apply { - isGone = !shouldShow + val label = button.requireViewById<MarqueedTextView>(R.id.label) + val image = button.requireViewById<ImageView>(R.id.image) + + button.isGone = !shouldShow + label.apply { setTextColor(style.textColor) - compoundDrawableTintList = ColorStateList.valueOf(style.textColor) + startMarquee() } + image.imageTintList = ColorStateList.valueOf(style.textColor) } } @@ -712,12 +733,17 @@ class HandleMenu( } else { getString(R.string.open_in_browser_text) } - openInAppOrBrowserBtn.apply { + + val label = openInAppOrBrowserBtn.requireViewById<MarqueedTextView>(R.id.label) + val image = openInAppOrBrowserBtn.requireViewById<ImageView>(R.id.image) + openInAppOrBrowserBtn.contentDescription = btnText + label.apply { text = btnText - contentDescription = btnText setTextColor(style.textColor) - compoundDrawableTintList = ColorStateList.valueOf(style.textColor) + startMarquee() } + image.imageTintList = ColorStateList.valueOf(style.textColor) + openByDefaultBtn.isGone = isBrowserApp openByDefaultBtn.imageTintList = ColorStateList.valueOf(style.textColor) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt index 470e5a1d88b4..75f90bb9c38e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt @@ -327,7 +327,7 @@ class HandleMenuAnimator( } // Open in Browser Button Opacity Animation - val button = openInAppOrBrowserPill.requireViewById<Button>(R.id.open_in_app_or_browser_button) + val button = openInAppOrBrowserPill.requireViewById<View>(R.id.open_in_app_or_browser_button) animators += ObjectAnimator.ofFloat(button, ALPHA, 1f).apply { startDelay = BODY_ALPHA_OPEN_DELAY diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MarqueedTextView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MarqueedTextView.kt new file mode 100644 index 000000000000..733b6221ac0e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MarqueedTextView.kt @@ -0,0 +1,48 @@ +/* + * 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.windowdecor + +import android.content.Context +import android.util.AttributeSet +import android.widget.TextView + +/** A custom [TextView] that allows better control over marquee animation used to ellipsize text. */ +class MarqueedTextView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = android.R.attr.textViewStyle +) : TextView(context, attrs, defStyleAttr) { + + /** + * Starts marquee animation if the layout attributes for this object include + * `android:ellipsize=marquee`, `android:singleLine=true`, and + * `android:scrollHorizontally=true`. + */ + override public fun startMarquee() { + super.startMarquee() + } + + /** + * Must always return [true] since [TextView.startMarquee()] requires view to be selected or + * focused in order to start the marquee animation. + * + * We are not using [TextView.setSelected()] as this would dispatch undesired accessibility + * events. + */ + override fun isSelected() : Boolean { + return true + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OWNERS index 3f828f547920..992402528f4f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OWNERS +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OWNERS @@ -1,3 +1,2 @@ -jorgegil@google.com mattsziklay@google.com mdehaini@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt index bc2be901d320..90c865e502fc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt @@ -228,23 +228,29 @@ class AppHeaderViewHolder( } } - with(context.resources) { - // Update a11y read out to say "double tap to maximize or restore window size" - ViewCompat.replaceAccessibilityAction( - maximizeWindowButton, - AccessibilityActionCompat.ACTION_CLICK, - getString(R.string.maximize_button_talkback_action_maximize_restore_text), - null - ) + // Update a11y announcement to say "double tap to open menu" + ViewCompat.replaceAccessibilityAction( + openMenuButton, + AccessibilityActionCompat.ACTION_CLICK, + context.getString(R.string.app_handle_chip_accessibility_announce), + null + ) - // Update a11y read out to say "double tap to minimize app window" - ViewCompat.replaceAccessibilityAction( - minimizeWindowButton, - AccessibilityActionCompat.ACTION_CLICK, - getString(R.string.minimize_button_talkback_action_maximize_restore_text), - null - ) - } + // Update a11y announcement to say "double tap to maximize or restore window size" + ViewCompat.replaceAccessibilityAction( + maximizeWindowButton, + AccessibilityActionCompat.ACTION_CLICK, + context.getString(R.string.maximize_button_talkback_action_maximize_restore_text), + null + ) + + // Update a11y announcement out to say "double tap to minimize app window" + ViewCompat.replaceAccessibilityAction( + minimizeWindowButton, + AccessibilityActionCompat.ACTION_CLICK, + context.getString(R.string.minimize_button_talkback_action_maximize_restore_text), + null + ) } override fun bindData(data: HeaderData) { @@ -260,6 +266,8 @@ class AppHeaderViewHolder( /** Sets the app's name in the header. */ fun setAppName(name: CharSequence) { appNameTextView.text = name + openMenuButton.contentDescription = + context.getString(R.string.desktop_mode_app_header_chip_text, name) } /** Sets the app's icon in the header. */ diff --git a/libs/WindowManager/Shell/tests/OWNERS b/libs/WindowManager/Shell/tests/OWNERS index 19829e7e5677..bac8e5062128 100644 --- a/libs/WindowManager/Shell/tests/OWNERS +++ b/libs/WindowManager/Shell/tests/OWNERS @@ -12,7 +12,6 @@ atsjenk@google.com jorgegil@google.com vaniadesmonda@google.com pbdr@google.com -tkachenkoi@google.com mpodolian@google.com jeremysim@google.com peanutbutter@google.com diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml index 02b2cec8dbdb..ae73dae99d6f 100644 --- a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml @@ -53,10 +53,12 @@ <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="teardown-command" value="settings delete secure show_ime_with_hard_keyboard"/> <option name="teardown-command" value="settings delete system show_touches"/> <option name="teardown-command" value="settings delete system pointer_location"/> + <option name="teardown-command" value="settings delete secure glanceable_hub_enabled"/> <option name="teardown-command" value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/> </target_preparer> diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java index ffcc3446d436..7a7d88b80ce3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java @@ -572,6 +572,22 @@ public class BubbleDataTest extends ShellTestCase { assertThat(update.shouldShowEducation).isTrue(); } + /** Verifies that the update should contain the bubble bar location. */ + @Test + public void test_shouldUpdateBubbleBarLocation() { + // Setup + mBubbleData.setListener(mListener); + + // Test + mBubbleData.notificationEntryUpdated(mBubbleA1, /* suppressFlyout */ true, /* showInShade */ + true, BubbleBarLocation.LEFT); + + // Verify + verifyUpdateReceived(); + BubbleData.Update update = mUpdateCaptor.getValue(); + assertThat(update.mBubbleBarLocation).isEqualTo(BubbleBarLocation.LEFT); + } + /** * Verifies that the update shouldn't show the user education, if the education is required but * the bubble should auto-expand @@ -1367,6 +1383,20 @@ public class BubbleDataTest extends ShellTestCase { } @Test + public void setSelectedBubbleAndExpandStackWithLocation() { + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + mBubbleData.setListener(mListener); + + mBubbleData.setSelectedBubbleAndExpandStack(mBubbleA1, BubbleBarLocation.LEFT); + + verifyUpdateReceived(); + assertSelectionChangedTo(mBubbleA1); + assertExpandedChangedTo(true); + assertLocationChangedTo(BubbleBarLocation.LEFT); + } + + @Test public void testShowOverflowChanged_hasOverflowBubbles() { assertThat(mBubbleData.getOverflowBubbles()).isEmpty(); sendUpdatedEntryAtTime(mEntryA1, 1000); @@ -1450,6 +1480,12 @@ public class BubbleDataTest extends ShellTestCase { assertWithMessage("selectedBubble").that(update.selectedBubble).isEqualTo(bubble); } + private void assertLocationChangedTo(BubbleBarLocation location) { + BubbleData.Update update = mUpdateCaptor.getValue(); + assertWithMessage("locationChanged").that(update.mBubbleBarLocation) + .isEqualTo(location); + } + private void assertExpandedChangedTo(boolean expected) { BubbleData.Update update = mUpdateCaptor.getValue(); assertWithMessage("expandedChanged").that(update.expandedChanged).isTrue(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowSessionSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowSessionSupplierTest.kt index 33e8d78d6a15..7b7d96c4294c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowSessionSupplierTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowSessionSupplierTest.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.common import android.testing.AndroidTestingRunner import android.view.IWindowSession import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase import org.junit.Test import org.junit.runner.RunWith @@ -30,10 +31,10 @@ import org.junit.runner.RunWith */ @RunWith(AndroidTestingRunner::class) @SmallTest -class WindowSessionSupplierTest { +class WindowSessionSupplierTest : ShellTestCase() { @Test - fun `InputChannelSupplier supplies an InputChannel`() { + fun `WindowSessionSupplierTest supplies an IWindowSession`() { val supplier = WindowSessionSupplier() SuppliersUtilsTest.assertSupplierProvidesValue(supplier) { it is IWindowSession 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 d6b13610c9c1..70a30a3ca7a9 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.isDeviceEligibleForDesktopMode(any()) } + doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(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 403d468a7034..d510570e8839 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 @@ -30,7 +30,6 @@ import android.view.KeyEvent import android.window.DisplayAreaInfo import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer -import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.dx.mockito.inline.extended.StaticMockitoSession import com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER @@ -48,7 +47,6 @@ import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction -import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.FocusTransitionObserver import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel @@ -107,12 +105,7 @@ class DesktopModeKeyGestureHandlerTest : ShellTestCase() { @Before fun setUp() { Dispatchers.setMain(StandardTestDispatcher()) - mockitoSession = - mockitoSession() - .strictness(Strictness.LENIENT) - .spyStatic(DesktopModeStatus::class.java) - .startMocking() - doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) } + mockitoSession = mockitoSession().strictness(Strictness.LENIENT).startMocking() 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 edb9b2d2fede..718bf322f6a9 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 @@ -171,7 +171,6 @@ import org.junit.Assume.assumeTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.isA import org.mockito.ArgumentMatchers.isNull import org.mockito.Mock @@ -292,7 +291,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() .spyStatic(DesktopModeStatus::class.java) .spyStatic(Toast::class.java) .startMocking() - doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) } + doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(any()) } testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) shellInit = spy(ShellInit(testExecutor)) @@ -363,9 +362,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() shellInit.init() - val captor = ArgumentCaptor.forClass(RecentsTransitionStateListener::class.java) + val captor = argumentCaptor<RecentsTransitionStateListener>() verify(recentsTransitionHandler).addTransitionStateListener(captor.capture()) - recentsTransitionStateListener = captor.value + recentsTransitionStateListener = captor.firstValue controller.taskbarDesktopTaskListener = taskbarDesktopTaskListener @@ -441,7 +440,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun doesAnyTaskRequireTaskbarRounding_toggleResizeOfFreeFormTask_returnTrue() { val task1 = setUpFreeformTask() - val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java) + val argumentCaptor = argumentCaptor<Boolean>() controller.toggleDesktopTaskSize( task1, ToggleTaskSizeInteraction( @@ -461,7 +460,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() STABLE_BOUNDS.height(), displayController, ) - assertThat(argumentCaptor.value).isTrue() + assertThat(argumentCaptor.firstValue).isTrue() } @Test @@ -476,7 +475,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val stableBounds = Rect().apply { displayLayout.getStableBounds(this) } val task1 = setUpFreeformTask(bounds = stableBounds, active = true) - val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java) + val argumentCaptor = argumentCaptor<Boolean>() controller.toggleDesktopTaskSize( task1, ToggleTaskSizeInteraction( @@ -497,7 +496,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() eq(displayController), anyOrNull(), ) - assertThat(argumentCaptor.value).isFalse() + assertThat(argumentCaptor.firstValue).isFalse() } @Test @@ -547,6 +546,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperEnabled() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() markTaskHidden(task1) @@ -581,7 +581,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) // Wallpaper is moved to front. - wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + wct.assertReorderAt(index = 0, wallpaperToken) // Desk is activated. verify(desksOrganizer).activateDesk(wct, deskId) } @@ -783,6 +783,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperEnabled() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() markTaskVisible(task1) @@ -825,7 +826,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) - fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperEnabled() { + fun showDesktopApps_someAppsInvisible_desktopWallpaperEnabled_reordersOnlyFreeformTasks() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() markTaskHidden(task1) @@ -842,6 +844,24 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() wct.assertReorderAt(index = 2, task2) } + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_someAppsInvisible_desktopWallpaperEnabled_reordersAll() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskHidden(task1) + markTaskVisible(task2) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: wallpaper intent, task1, task2 + wct.assertReorderAt(index = 0, wallpaperToken) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + @Test @DisableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, @@ -860,9 +880,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_noActiveTasks_addDesktopWallpaper_desktopWallpaperEnabled() { - whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) - .thenReturn(Binder()) + fun showDesktopApps_noActiveTasks_desktopWallpaperEnabled_addsDesktopWallpaper() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) val wct = @@ -871,10 +891,18 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - @DisableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, - ) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_noActiveTasks_desktopWallpaperEnabled_reordersDesktopWallpaper() { + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + wct.assertReorderAt(index = 0, wallpaperToken) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperDisabled() { taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) @@ -899,6 +927,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperEnabled() { whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) .thenReturn(Binder()) + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY) @@ -991,6 +1020,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() /** TODO: b/362720497 - add multi-desk version when minimization is implemented. */ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun showDesktopApps_desktopWallpaperEnabled_dontReorderMinimizedTask() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val homeTask = setUpHomeTask() val freeformTask = setUpFreeformTask() val minimizedTask = setUpFreeformTask() @@ -1569,6 +1599,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun moveTaskToDesktop_desktopWallpaperEnabled_nonRunningTask_launchesInFreeform() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val task = createTaskInfo(1) whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task) @@ -1736,7 +1767,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun moveBackgroundTaskToDesktop_remoteTransition_usesOneShotHandler() { - val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) + val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>() whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())) .thenReturn(Binder()) @@ -1751,12 +1782,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() verify(desktopModeEnterExitTransitionListener) .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) - assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value) + assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.firstValue) } @Test fun moveRunningTaskToDesktop_remoteTransition_usesOneShotHandler() { - val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) + val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>() whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())) .thenReturn(Binder()) @@ -1768,7 +1799,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() verify(desktopModeEnterExitTransitionListener) .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) - assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value) + assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.firstValue) } @Test @@ -1802,6 +1833,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun moveRunningTaskToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperEnabled() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val freeformTask = setUpFreeformTask() val fullscreenTask = setUpFullscreenTask() markTaskHidden(freeformTask) @@ -1828,6 +1860,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, ) fun moveRunningTaskToDesktop_desktopWallpaperEnabled_multiDesksEnabled() { val freeformTask = setUpFreeformTask() @@ -1840,7 +1873,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() ) val wct = getLatestEnterDesktopWct() - wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + wct.assertReorderAt(index = 0, wallpaperToken) verify(desksOrganizer).moveTaskToDesk(wct, deskId = 0, fullscreenTask) verify(desksOrganizer).activateDesk(wct, deskId = 0) verify(desktopModeEnterExitTransitionListener) @@ -1967,6 +2000,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun moveRunningTaskToDesktop_desktopWallpaperEnabled_bringsTasksOverLimit_dontShowBackTask() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } val newTask = setUpFullscreenTask() val homeTask = setUpHomeTask() @@ -2224,26 +2258,26 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun moveTaskToFront_remoteTransition_usesOneshotHandler() { setUpHomeTask() val freeformTasks = List(MAX_TASK_LIMIT) { setUpFreeformTask() } - val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) + val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>() whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())) .thenReturn(Binder()) controller.moveTaskToFront(freeformTasks[0], RemoteTransition(TestRemoteTransition())) - assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value) + assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.firstValue) } @Test fun moveTaskToFront_bringsTasksOverLimit_remoteTransition_usesWindowLimitHandler() { setUpHomeTask() val freeformTasks = List(MAX_TASK_LIMIT + 1) { setUpFreeformTask() } - val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) + val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>() whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())) .thenReturn(Binder()) controller.moveTaskToFront(freeformTasks[0], RemoteTransition(TestRemoteTransition())) - assertThat(transitionHandlerArgCaptor.value) + assertThat(transitionHandlerArgCaptor.firstValue) .isInstanceOf(DesktopWindowLimitRemoteHandler::class.java) } @@ -2718,9 +2752,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> + captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() } } @@ -2759,9 +2793,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startPipTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> + captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() } } @@ -2775,9 +2809,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK } + captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK } } @Test @@ -2791,10 +2825,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() // The only active task is being minimized. controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) // Adds remove wallpaper operation - captor.value.assertReorderAt(index = 0, wallpaperToken, toTop = false) + captor.firstValue.assertReorderAt(index = 0, wallpaperToken, toTop = false) } @Test @@ -2808,9 +2842,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() // The only active task is already minimized. controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> + captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() } } @@ -2825,9 +2859,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.minimizeTask(task1, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> + captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() } } @@ -2845,10 +2879,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() // task1 is the only visible task as task2 is minimized. controller.minimizeTask(task1, MinimizeReason.MINIMIZE_BUTTON) // Adds remove wallpaper operation - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) // Adds remove wallpaper operation - captor.value.assertReorderAt(index = 0, wallpaperToken, toTop = false) + captor.firstValue.assertReorderAt(index = 0, wallpaperToken, toTop = false) } @Test @@ -2987,6 +3021,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_fullscreenTask_noTasks_enforceDesktop_freeformDisplay_returnFreeformWCT() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM @@ -3118,6 +3153,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_desktopWallpaperEnabled_freeformNotVisible_reorderedToTop() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val freeformTask1 = setUpFreeformTask() val freeformTask2 = createFreeformTask() @@ -3152,7 +3188,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_desktopWallpaperEnabled_noOtherTasks_reorderedToTop() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val task = createFreeformTask() + val result = controller.handleRequest(Binder(), createTransition(task)) assertNotNull(result, "Should handle request") @@ -3180,6 +3218,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_dskWallpaperEnabled_freeformOnOtherDisplayOnly_reorderedToTop() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY) // Second display task createFreeformTask(displayId = SECOND_DISPLAY) @@ -4635,7 +4674,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false) - val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val wctArgument = argumentCaptor<WindowContainerTransaction>() verify(splitScreenController) .requestEnterSplitSelect( eq(task2), @@ -4643,9 +4682,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT), eq(task2.configuration.windowConfiguration.bounds), ) - assertThat(wctArgument.value.hierarchyOps).hasSize(1) + assertThat(wctArgument.firstValue.hierarchyOps).hasSize(1) // Removes wallpaper activity when leaving desktop - wctArgument.value.assertReorderAt(index = 0, wallpaperToken, toTop = false) + wctArgument.firstValue.assertReorderAt(index = 0, wallpaperToken, toTop = false) } @Test @@ -4660,7 +4699,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false) - val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val wctArgument = argumentCaptor<WindowContainerTransaction>() verify(splitScreenController) .requestEnterSplitSelect( eq(task2), @@ -4669,7 +4708,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() eq(task2.configuration.windowConfiguration.bounds), ) // Does not remove wallpaper activity, as desktop still has visible desktop tasks - assertThat(wctArgument.value.hierarchyOps).isEmpty() + assertThat(wctArgument.firstValue.hierarchyOps).isEmpty() } @Test @@ -4677,7 +4716,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun newWindow_fromFullscreenOpensInSplit() { setUpLandscapeDisplay() val task = setUpFullscreenTask() - val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + val optionsCaptor = argumentCaptor<Bundle>() runOpenNewWindow(task) verify(splitScreenController) .startIntent( @@ -4690,7 +4729,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() eq(true), eq(SPLIT_INDEX_UNDEFINED), ) - assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode) .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) } @@ -4699,7 +4738,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun newWindow_fromSplitOpensInSplit() { setUpLandscapeDisplay() val task = setUpSplitScreenTask() - val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + val optionsCaptor = argumentCaptor<Bundle>() runOpenNewWindow(task) verify(splitScreenController) .startIntent( @@ -4712,7 +4751,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() eq(true), eq(SPLIT_INDEX_UNDEFINED), ) - assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode) .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) } @@ -4807,11 +4846,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() setUpLandscapeDisplay() val task = setUpFullscreenTask() val taskToRequest = setUpFreeformTask() - val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + val optionsCaptor = argumentCaptor<Bundle>() runOpenInstance(task, taskToRequest.taskId) verify(splitScreenController) .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull()) - assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode) .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) } @@ -4821,11 +4860,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() setUpLandscapeDisplay() val task = setUpSplitScreenTask() val taskToRequest = setUpFreeformTask() - val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + val optionsCaptor = argumentCaptor<Bundle>() runOpenInstance(task, taskToRequest.taskId) verify(splitScreenController) .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull()) - assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode) .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) } @@ -5427,38 +5466,90 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun onUnhandledDrag_newFreeformIntent() { + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX) + fun onUnhandledDrag_newFreeformIntent_tabTearingAnimationBugfixFlagEnabled() { + testOnUnhandledDrag( + DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR, + PointF(1200f, 700f), + Rect(240, 700, 2160, 1900), + tabTearingAnimationFlagEnabled = true, + ) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX) + fun onUnhandledDrag_newFreeformIntent_tabTearingAnimationBugfixFlagDisabled() { testOnUnhandledDrag( DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR, PointF(1200f, 700f), Rect(240, 700, 2160, 1900), + tabTearingAnimationFlagEnabled = false, + ) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX) + fun onUnhandledDrag_newFreeformIntentSplitLeft_tabTearingAnimationBugfixFlagEnabled() { + testOnUnhandledDrag( + DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR, + PointF(50f, 700f), + Rect(0, 0, 500, 1000), + tabTearingAnimationFlagEnabled = true, ) } @Test - fun onUnhandledDrag_newFreeformIntentSplitLeft() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX) + fun onUnhandledDrag_newFreeformIntentSplitLeft_tabTearingAnimationBugfixFlagDisabled() { testOnUnhandledDrag( DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR, PointF(50f, 700f), Rect(0, 0, 500, 1000), + tabTearingAnimationFlagEnabled = false, + ) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX) + fun onUnhandledDrag_newFreeformIntentSplitRight_tabTearingAnimationBugfixFlagEnabled() { + testOnUnhandledDrag( + DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR, + PointF(2500f, 700f), + Rect(500, 0, 1000, 1000), + tabTearingAnimationFlagEnabled = true, ) } @Test - fun onUnhandledDrag_newFreeformIntentSplitRight() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX) + fun onUnhandledDrag_newFreeformIntentSplitRight_tabTearingAnimationBugfixFlagDisabled() { testOnUnhandledDrag( DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR, PointF(2500f, 700f), Rect(500, 0, 1000, 1000), + tabTearingAnimationFlagEnabled = false, + ) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX) + fun onUnhandledDrag_newFullscreenIntent_tabTearingAnimationBugfixFlagEnabled() { + testOnUnhandledDrag( + DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, + PointF(1200f, 50f), + Rect(), + tabTearingAnimationFlagEnabled = true, ) } @Test - fun onUnhandledDrag_newFullscreenIntent() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX) + fun onUnhandledDrag_newFullscreenIntent_tabTearingAnimationBugfixFlagDisabled() { testOnUnhandledDrag( DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, PointF(1200f, 50f), Rect(), + tabTearingAnimationFlagEnabled = false, ) } @@ -5812,6 +5903,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() indicatorType: DesktopModeVisualIndicator.IndicatorType, inputCoordinate: PointF, expectedBounds: Rect, + tabTearingAnimationFlagEnabled: Boolean, ) { setUpLandscapeDisplay() val task = setUpFreeformTask() @@ -5842,6 +5934,16 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() anyOrNull(), eq(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT), ) + whenever( + desktopMixedTransitionHandler.startLaunchTransition( + eq(TRANSIT_OPEN), + any(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + ) + .thenReturn(Binder()) spyController.onUnhandledDrag( mockPendingIntent, @@ -5849,24 +5951,37 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() mockDragEvent, mockCallback as Consumer<Boolean>, ) - val arg: ArgumentCaptor<WindowContainerTransaction> = - ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() var expectedWindowingMode: Int if (indicatorType == DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) { expectedWindowingMode = WINDOWING_MODE_FULLSCREEN // Fullscreen launches currently use default transitions - verify(transitions).startTransition(any(), capture(arg), anyOrNull()) + verify(transitions).startTransition(any(), arg.capture(), anyOrNull()) } else { expectedWindowingMode = WINDOWING_MODE_FREEFORM - // All other launches use a special handler. - verify(dragAndDropTransitionHandler).handleDropEvent(capture(arg)) + if (tabTearingAnimationFlagEnabled) { + verify(desktopMixedTransitionHandler) + .startLaunchTransition( + eq(TRANSIT_OPEN), + arg.capture(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } else { + // All other launches use a special handler. + verify(dragAndDropTransitionHandler).handleDropEvent(arg.capture()) + } } assertThat( - ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions) + ActivityOptions.fromBundle(arg.firstValue.hierarchyOps[0].launchOptions) .launchWindowingMode ) .isEqualTo(expectedWindowingMode) - assertThat(ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions).launchBounds) + assertThat( + ActivityOptions.fromBundle(arg.firstValue.hierarchyOps[0].launchOptions) + .launchBounds + ) .isEqualTo(expectedBounds) } @@ -6048,52 +6163,49 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @WindowManager.TransitionType type: Int = TRANSIT_OPEN, handlerClass: Class<out TransitionHandler>? = null, ): WindowContainerTransaction { - val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() if (handlerClass == null) { verify(transitions).startTransition(eq(type), arg.capture(), isNull()) } else { verify(transitions).startTransition(eq(type), arg.capture(), isA(handlerClass)) } - return arg.value + return arg.lastValue } private fun getLatestToggleResizeDesktopTaskWct( currentBounds: Rect? = null ): WindowContainerTransaction { - val arg: ArgumentCaptor<WindowContainerTransaction> = - ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce()) - .startTransition(capture(arg), eq(currentBounds)) - return arg.value + .startTransition(arg.capture(), eq(currentBounds)) + return arg.lastValue } private fun getLatestDesktopMixedTaskWct( @WindowManager.TransitionType type: Int = TRANSIT_OPEN ): WindowContainerTransaction { - val arg: ArgumentCaptor<WindowContainerTransaction> = - ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() verify(desktopMixedTransitionHandler) - .startLaunchTransition(eq(type), capture(arg), anyOrNull(), anyOrNull(), anyOrNull()) - return arg.value + .startLaunchTransition(eq(type), arg.capture(), anyOrNull(), anyOrNull(), anyOrNull()) + return arg.lastValue } private fun getLatestEnterDesktopWct(): WindowContainerTransaction { - val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture(), any()) - return arg.value + return arg.lastValue } private fun getLatestDragToDesktopWct(): WindowContainerTransaction { - val arg: ArgumentCaptor<WindowContainerTransaction> = - ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(capture(arg)) - return arg.value + val arg = argumentCaptor<WindowContainerTransaction>() + verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(arg.capture()) + return arg.lastValue } private fun getLatestExitDesktopWct(): WindowContainerTransaction { - val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() verify(exitDesktopTransitionHandler).startTransition(any(), arg.capture(), any(), any()) - return arg.value + return arg.lastValue } private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? = diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt index 83e48728c4f2..030bb1ace49d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt @@ -123,8 +123,26 @@ class DesktopUserRepositoriesTest : ShellTestCase() { assertThat(desktopRepository.userId).isEqualTo(PROFILE_ID_2) } + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_HSUM) + fun getUserForProfile_flagEnabled_returnsUserIdForProfile() { + userRepositories.onUserChanged(USER_ID_2, mock()) + val profiles: MutableList<UserInfo> = + mutableListOf( + UserInfo(USER_ID_2, "User profile", 0), + UserInfo(PROFILE_ID_1, "Work profile", 0), + ) + userRepositories.onUserProfilesChanged(profiles) + + val userIdForProfile = userRepositories.getUserIdForProfile(PROFILE_ID_1) + + assertThat(userIdForProfile).isEqualTo(USER_ID_2) + } + private companion object { const val USER_ID_1 = 7 + const val USER_ID_2 = 8 + const val PROFILE_ID_1 = 4 const val PROFILE_ID_2 = 5 } } 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 25246d9984c3..1732875f1d57 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 @@ -70,6 +70,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor @Mock private lateinit var draggedTaskLeash: SurfaceControl @Mock private lateinit var homeTaskLeash: SurfaceControl + @Mock private lateinit var desktopUserRepositories: DesktopUserRepositories private val transactionSupplier = Supplier { mock<SurfaceControl.Transaction>() } @@ -84,6 +85,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { context, transitions, taskDisplayAreaOrganizer, + desktopUserRepositories, mockInteractionJankMonitor, transactionSupplier, ) @@ -93,6 +95,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { context, transitions, taskDisplayAreaOrganizer, + desktopUserRepositories, mockInteractionJankMonitor, transactionSupplier, ) @@ -484,17 +487,22 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { val mergedFinishTransaction = mock<SurfaceControl.Transaction>() val finishCallback = mock<Transitions.TransitionFinishCallback>() val task = createTask() - val startTransition = startDrag( - springHandler, task, finishTransaction = playingFinishTransaction, homeChange = null) + 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, - ), + createTransitionInfo( + type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, + draggedTask = task, + ), startT = mergedStartTransaction, finishT = mergedFinishTransaction, mergeTarget = startTransition, @@ -723,7 +731,8 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { private fun createTransitionInfo( type: Int, draggedTask: RunningTaskInfo, - homeChange: TransitionInfo.Change? = createHomeChange()) = + homeChange: TransitionInfo.Change? = createHomeChange(), + ) = TransitionInfo(type, /* flags= */ 0).apply { homeChange?.let { addChange(it) } addChange( // Dragged Task. @@ -741,11 +750,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 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/recents/RecentsTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java index b50af741b2a6..439be9155b26 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java @@ -17,9 +17,13 @@ package com.android.wm.shell.recents; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX; import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_ANIMATING; import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING; import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED; @@ -44,9 +48,11 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.content.res.Resources; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; +import android.platform.test.annotations.EnableFlags; import android.view.SurfaceControl; import android.window.TransitionInfo; @@ -57,6 +63,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.dx.mockito.inline.extended.StaticMockitoSession; import com.android.internal.os.IResultReceiver; +import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; @@ -92,9 +99,13 @@ import java.util.Optional; @SmallTest public class RecentsTransitionHandlerTest extends ShellTestCase { + private static final int FREEFORM_TASK_CORNER_RADIUS = 32; + @Mock private Context mContext; @Mock + private Resources mResources; + @Mock private TaskStackListenerImpl mTaskStackListener; @Mock private ShellCommandHandler mShellCommandHandler; @@ -134,6 +145,10 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class)); when(mContext.getSystemService(KeyguardManager.class)) .thenReturn(mock(KeyguardManager.class)); + when(mContext.getResources()).thenReturn(mResources); + when(mResources.getDimensionPixelSize( + R.dimen.desktop_windowing_freeform_rounded_corner_radius) + ).thenReturn(FREEFORM_TASK_CORNER_RADIUS); mShellInit = spy(new ShellInit(mMainExecutor)); mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler, mDisplayInsetsController, mMainExecutor)); @@ -276,6 +291,57 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { assertThat(listener.getState()).isEqualTo(TRANSITION_STATE_NOT_RUNNING); } + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + public void testMergeAndFinish_openingFreeformTasks_setsCornerRadius() { + ActivityManager.RunningTaskInfo freeformTask = + new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + TransitionInfo mergeTransitionInfo = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN, freeformTask) + .build(); + SurfaceControl leash = mergeTransitionInfo.getChanges().get(0).getLeash(); + final IBinder transition = startRecentsTransition(/* synthetic= */ false); + SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + mRecentsTransitionHandler.startAnimation( + transition, createTransitionInfo(), new StubTransaction(), new StubTransaction(), + mock(Transitions.TransitionFinishCallback.class)); + + mRecentsTransitionHandler.findController(transition).merge( + mergeTransitionInfo, + new StubTransaction(), + finishT, + transition, + mock(Transitions.TransitionFinishCallback.class)); + mRecentsTransitionHandler.findController(transition).finish(/* toHome= */ false, + false /* sendUserLeaveHint */, mock(IResultReceiver.class)); + mMainExecutor.flushAll(); + + verify(finishT).setCornerRadius(leash, FREEFORM_TASK_CORNER_RADIUS); + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + public void testFinish_returningToFreeformTasks_setsCornerRadius() { + ActivityManager.RunningTaskInfo freeformTask = + new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + TransitionInfo transitionInfo = new TransitionInfoBuilder(TRANSIT_CLOSE) + .addChange(TRANSIT_CLOSE, freeformTask) + .build(); + SurfaceControl leash = transitionInfo.getChanges().get(0).getLeash(); + final IBinder transition = startRecentsTransition(/* synthetic= */ false); + SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + mRecentsTransitionHandler.startAnimation( + transition, transitionInfo, new StubTransaction(), finishT, + mock(Transitions.TransitionFinishCallback.class)); + + mRecentsTransitionHandler.findController(transition).finish(/* toHome= */ false, + false /* sendUserLeaveHint */, mock(IResultReceiver.class)); + mMainExecutor.flushAll(); + + + verify(finishT).setCornerRadius(leash, FREEFORM_TASK_CORNER_RADIUS); + } + private IBinder startRecentsTransition(boolean synthetic) { return startRecentsTransition(synthetic, mock(IRecentsAnimationRunner.class)); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt index e28d6ff8bf7f..fd22a84dee5d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt @@ -16,8 +16,10 @@ package com.android.wm.shell.shared.bubbles +import android.content.Context import android.graphics.Insets import android.graphics.Rect +import androidx.test.core.app.ApplicationProvider.getApplicationContext import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.wm.shell.shared.bubbles.DragZoneFactory.DesktopWindowModeChecker @@ -27,11 +29,14 @@ import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith +private typealias DragZoneVerifier = (dragZone: DragZone) -> Unit + @SmallTest @RunWith(AndroidJUnit4::class) /** Unit tests for [DragZoneFactory]. */ class DragZoneFactoryTest { + private val context = getApplicationContext<Context>() private lateinit var dragZoneFactory: DragZoneFactory private val tabletPortrait = DeviceConfig( @@ -55,184 +60,238 @@ class DragZoneFactoryTest { @Test fun dragZonesForBubbleBar_tablet() { dragZoneFactory = - DragZoneFactory(tabletPortrait, splitScreenModeChecker, desktopWindowModeChecker) + DragZoneFactory( + context, + tabletPortrait, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = dragZoneFactory.createSortedDragZones(DraggedObject.BubbleBar(BubbleBarLocation.LEFT)) - val expectedZones: List<Class<out DragZone>> = + val expectedZones: List<DragZoneVerifier> = listOf( - DragZone.Dismiss::class.java, - DragZone.Bubble::class.java, - DragZone.Bubble::class.java, + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), ) - dragZones.zip(expectedZones).forEach { (zone, expectedType) -> - assertThat(zone).isInstanceOf(expectedType) - } + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } } @Test fun dragZonesForBubble_tablet_portrait() { dragZoneFactory = - DragZoneFactory(tabletPortrait, splitScreenModeChecker, desktopWindowModeChecker) + DragZoneFactory( + context, + tabletPortrait, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT)) - val expectedZones: List<Class<out DragZone>> = + val expectedZones: List<DragZoneVerifier> = listOf( - DragZone.Dismiss::class.java, - DragZone.Bubble.Left::class.java, - DragZone.Bubble.Right::class.java, - DragZone.FullScreen::class.java, - DragZone.DesktopWindow::class.java, - DragZone.Split.Top::class.java, - DragZone.Split.Bottom::class.java, - ) - dragZones.zip(expectedZones).forEach { (zone, expectedType) -> - assertThat(zone).isInstanceOf(expectedType) - } + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.DesktopWindow>(), + verifyInstance<DragZone.Split.Top>(), + verifyInstance<DragZone.Split.Bottom>(), + ) + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } } @Test fun dragZonesForBubble_tablet_landscape() { - dragZoneFactory = DragZoneFactory(tabletLandscape, splitScreenModeChecker, desktopWindowModeChecker) + dragZoneFactory = + DragZoneFactory( + context, + tabletLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT)) - val expectedZones: List<Class<out DragZone>> = + val expectedZones: List<DragZoneVerifier> = listOf( - DragZone.Dismiss::class.java, - DragZone.Bubble.Left::class.java, - DragZone.Bubble.Right::class.java, - DragZone.FullScreen::class.java, - DragZone.DesktopWindow::class.java, - DragZone.Split.Left::class.java, - DragZone.Split.Right::class.java, - ) - dragZones.zip(expectedZones).forEach { (zone, expectedType) -> - assertThat(zone).isInstanceOf(expectedType) - } + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.DesktopWindow>(), + verifyInstance<DragZone.Split.Left>(), + verifyInstance<DragZone.Split.Right>(), + ) + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } } @Test fun dragZonesForBubble_foldable_portrait() { - dragZoneFactory = DragZoneFactory(foldablePortrait, splitScreenModeChecker, desktopWindowModeChecker) + dragZoneFactory = + DragZoneFactory( + context, + foldablePortrait, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT)) - val expectedZones: List<Class<out DragZone>> = + val expectedZones: List<DragZoneVerifier> = listOf( - DragZone.Dismiss::class.java, - DragZone.Bubble.Left::class.java, - DragZone.Bubble.Right::class.java, - DragZone.FullScreen::class.java, - DragZone.Split.Left::class.java, - DragZone.Split.Right::class.java, - ) - dragZones.zip(expectedZones).forEach { (zone, expectedType) -> - assertThat(zone).isInstanceOf(expectedType) - } + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.Split.Left>(), + verifyInstance<DragZone.Split.Right>(), + ) + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } } @Test fun dragZonesForBubble_foldable_landscape() { - dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker) + dragZoneFactory = + DragZoneFactory( + context, + foldableLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT)) - val expectedZones: List<Class<out DragZone>> = + val expectedZones: List<DragZoneVerifier> = listOf( - DragZone.Dismiss::class.java, - DragZone.Bubble.Left::class.java, - DragZone.Bubble.Right::class.java, - DragZone.FullScreen::class.java, - DragZone.Split.Top::class.java, - DragZone.Split.Bottom::class.java, - ) - dragZones.zip(expectedZones).forEach { (zone, expectedType) -> - assertThat(zone).isInstanceOf(expectedType) - } + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.Split.Top>(), + verifyInstance<DragZone.Split.Bottom>(), + ) + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } } @Test fun dragZonesForExpandedView_tablet_portrait() { dragZoneFactory = - DragZoneFactory(tabletPortrait, splitScreenModeChecker, desktopWindowModeChecker) + DragZoneFactory( + context, + tabletPortrait, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = dragZoneFactory.createSortedDragZones( DraggedObject.ExpandedView(BubbleBarLocation.LEFT) ) - val expectedZones: List<Class<out DragZone>> = + val expectedZones: List<DragZoneVerifier> = listOf( - DragZone.Dismiss::class.java, - DragZone.FullScreen::class.java, - DragZone.DesktopWindow::class.java, - DragZone.Split.Top::class.java, - DragZone.Split.Bottom::class.java, - DragZone.Bubble.Left::class.java, - DragZone.Bubble.Right::class.java, - ) - dragZones.zip(expectedZones).forEach { (zone, expectedType) -> - assertThat(zone).isInstanceOf(expectedType) - } + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.DesktopWindow>(), + verifyInstance<DragZone.Split.Top>(), + verifyInstance<DragZone.Split.Bottom>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), + ) + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } } @Test fun dragZonesForExpandedView_tablet_landscape() { - dragZoneFactory = DragZoneFactory(tabletLandscape, splitScreenModeChecker, desktopWindowModeChecker) + dragZoneFactory = + DragZoneFactory( + context, + tabletLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = - dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT)) - val expectedZones: List<Class<out DragZone>> = + dragZoneFactory.createSortedDragZones( + DraggedObject.ExpandedView(BubbleBarLocation.LEFT) + ) + val expectedZones: List<DragZoneVerifier> = listOf( - DragZone.Dismiss::class.java, - DragZone.FullScreen::class.java, - DragZone.DesktopWindow::class.java, - DragZone.Split.Left::class.java, - DragZone.Split.Right::class.java, - DragZone.Bubble.Left::class.java, - DragZone.Bubble.Right::class.java, - ) - dragZones.zip(expectedZones).forEach { (zone, expectedType) -> - assertThat(zone).isInstanceOf(expectedType) - } + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.DesktopWindow>(), + verifyInstance<DragZone.Split.Left>(), + verifyInstance<DragZone.Split.Right>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), + ) + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } } @Test fun dragZonesForExpandedView_foldable_portrait() { - dragZoneFactory = DragZoneFactory(foldablePortrait, splitScreenModeChecker, desktopWindowModeChecker) + dragZoneFactory = + DragZoneFactory( + context, + foldablePortrait, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = - dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT)) - val expectedZones: List<Class<out DragZone>> = + dragZoneFactory.createSortedDragZones( + DraggedObject.ExpandedView(BubbleBarLocation.LEFT) + ) + val expectedZones: List<DragZoneVerifier> = listOf( - DragZone.Dismiss::class.java, - DragZone.FullScreen::class.java, - DragZone.Split.Left::class.java, - DragZone.Split.Right::class.java, - DragZone.Bubble.Left::class.java, - DragZone.Bubble.Right::class.java, - ) - dragZones.zip(expectedZones).forEach { (zone, expectedType) -> - assertThat(zone).isInstanceOf(expectedType) - } + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.Split.Left>(), + verifyInstance<DragZone.Split.Right>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), + ) + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } } @Test fun dragZonesForExpandedView_foldable_landscape() { - dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker) + dragZoneFactory = + DragZoneFactory( + context, + foldableLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = - dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT)) - val expectedZones: List<Class<out DragZone>> = + dragZoneFactory.createSortedDragZones( + DraggedObject.ExpandedView(BubbleBarLocation.LEFT) + ) + val expectedZones: List<DragZoneVerifier> = listOf( - DragZone.Dismiss::class.java, - DragZone.FullScreen::class.java, - DragZone.Split.Top::class.java, - DragZone.Split.Bottom::class.java, - DragZone.Bubble.Left::class.java, - DragZone.Bubble.Right::class.java, - ) - dragZones.zip(expectedZones).forEach { (zone, expectedType) -> - assertThat(zone).isInstanceOf(expectedType) - } + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.Split.Top>(), + verifyInstance<DragZone.Split.Bottom>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), + ) + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } } @Test fun dragZonesForBubble_tablet_desktopModeDisabled() { isDesktopWindowModeSupported = false - dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker) + dragZoneFactory = + DragZoneFactory( + context, + foldableLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT)) assertThat(dragZones.filterIsInstance<DragZone.DesktopWindow>()).isEmpty() @@ -241,9 +300,21 @@ class DragZoneFactoryTest { @Test fun dragZonesForExpandedView_tablet_desktopModeDisabled() { isDesktopWindowModeSupported = false - dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker) + dragZoneFactory = + DragZoneFactory( + context, + foldableLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = - dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT)) + dragZoneFactory.createSortedDragZones( + DraggedObject.ExpandedView(BubbleBarLocation.LEFT) + ) assertThat(dragZones.filterIsInstance<DragZone.DesktopWindow>()).isEmpty() } + + private inline fun <reified T> verifyInstance(): DragZoneVerifier = { dragZone -> + assertThat(dragZone).isInstanceOf(T::class.java) + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt new file mode 100644 index 000000000000..efb91c5fbfda --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt @@ -0,0 +1,191 @@ +/* + * 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.shared.bubbles + +import android.graphics.Rect +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.test.assertFails + +/** Unit tests for [DropTargetManager]. */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class DropTargetManagerTest { + + private lateinit var dropTargetManager: DropTargetManager + private lateinit var dragZoneChangedListener: FakeDragZoneChangedListener + private val dropTarget = Rect(0, 0, 0, 0) + + // create 3 drop zones that are horizontally next to each other + // ------------------------------------------------- + // | | | | + // | bubble | | bubble | + // | | dismiss | | + // | left | | right | + // | | | | + // ------------------------------------------------- + private val bubbleLeftDragZone = + DragZone.Bubble.Left(bounds = Rect(0, 0, 100, 100), dropTarget = dropTarget) + private val dismissDragZone = DragZone.Dismiss(bounds = Rect(100, 0, 200, 100)) + private val bubbleRightDragZone = + DragZone.Bubble.Right(bounds = Rect(200, 0, 300, 100), dropTarget = dropTarget) + + @Before + fun setUp() { + dragZoneChangedListener = FakeDragZoneChangedListener() + dropTargetManager = DropTargetManager(isLayoutRtl = false, dragZoneChangedListener) + } + + @Test + fun onDragStarted_notifiesInitialDragZone() { + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.LEFT), + listOf(bubbleLeftDragZone, bubbleRightDragZone) + ) + assertThat(dragZoneChangedListener.initialDragZone).isEqualTo(bubbleLeftDragZone) + } + + @Test + fun onDragStarted_missingExpectedDragZone_fails() { + assertFails { + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.RIGHT), + listOf(bubbleLeftDragZone) + ) + } + } + + @Test + fun onDragUpdated_notifiesDragZoneChanged() { + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.LEFT), + listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone) + ) + dropTargetManager.onDragUpdated( + bubbleRightDragZone.bounds.centerX(), + bubbleRightDragZone.bounds.centerY() + ) + assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleLeftDragZone) + assertThat(dragZoneChangedListener.toDragZone).isEqualTo(bubbleRightDragZone) + + dropTargetManager.onDragUpdated( + dismissDragZone.bounds.centerX(), + dismissDragZone.bounds.centerY() + ) + assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleRightDragZone) + assertThat(dragZoneChangedListener.toDragZone).isEqualTo(dismissDragZone) + } + + @Test + fun onDragUpdated_withinSameZone_doesNotNotify() { + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.LEFT), + listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone) + ) + dropTargetManager.onDragUpdated( + bubbleLeftDragZone.bounds.centerX(), + bubbleLeftDragZone.bounds.centerY() + ) + assertThat(dragZoneChangedListener.fromDragZone).isNull() + assertThat(dragZoneChangedListener.toDragZone).isNull() + } + + @Test + fun onDragUpdated_outsideAllZones_doesNotNotify() { + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.LEFT), + listOf(bubbleLeftDragZone, bubbleRightDragZone) + ) + val pointX = 200 + val pointY = 200 + assertThat(bubbleLeftDragZone.contains(pointX, pointY)).isFalse() + assertThat(bubbleRightDragZone.contains(pointX, pointY)).isFalse() + dropTargetManager.onDragUpdated(pointX, pointY) + assertThat(dragZoneChangedListener.fromDragZone).isNull() + assertThat(dragZoneChangedListener.toDragZone).isNull() + } + + @Test + fun onDragUpdated_hasOverlappingZones_notifiesFirstDragZoneChanged() { + // create a drag zone that spans across the width of all 3 drag zones, but extends below + // them + val splitDragZone = DragZone.Split.Left(bounds = Rect(0, 0, 300, 200)) + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.LEFT), + listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone, splitDragZone) + ) + + // drag to a point that is within both the bubble right zone and split zone + val (pointX, pointY) = + Pair( + bubbleRightDragZone.bounds.centerX(), + bubbleRightDragZone.bounds.centerY() + ) + assertThat(splitDragZone.contains(pointX, pointY)).isTrue() + dropTargetManager.onDragUpdated(pointX, pointY) + // verify we dragged to the bubble right zone because that has higher priority than split + assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleLeftDragZone) + assertThat(dragZoneChangedListener.toDragZone).isEqualTo(bubbleRightDragZone) + + dropTargetManager.onDragUpdated( + bubbleRightDragZone.bounds.centerX(), + 150 // below the bubble and dismiss drag zones but within split + ) + assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleRightDragZone) + assertThat(dragZoneChangedListener.toDragZone).isEqualTo(splitDragZone) + + val (dismissPointX, dismissPointY) = + Pair(dismissDragZone.bounds.centerX(), dismissDragZone.bounds.centerY()) + assertThat(splitDragZone.contains(dismissPointX, dismissPointY)).isTrue() + dropTargetManager.onDragUpdated(dismissPointX, dismissPointY) + assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(splitDragZone) + assertThat(dragZoneChangedListener.toDragZone).isEqualTo(dismissDragZone) + } + + @Test + fun onDragUpdated_afterDragEnded_doesNotNotify() { + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.LEFT), + listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone) + ) + dropTargetManager.onDragEnded() + dropTargetManager.onDragUpdated( + bubbleRightDragZone.bounds.centerX(), + bubbleRightDragZone.bounds.centerY() + ) + assertThat(dragZoneChangedListener.fromDragZone).isNull() + assertThat(dragZoneChangedListener.toDragZone).isNull() + } + + private class FakeDragZoneChangedListener : DropTargetManager.DragZoneChangedListener { + var initialDragZone: DragZone? = null + var fromDragZone: DragZone? = null + var toDragZone: DragZone? = null + + override fun onInitialDragZoneSet(dragZone: DragZone) { + initialDragZone = dragZone + } + override fun onDragZoneChanged(from: DragZone, to: DragZone) { + fromDragZone = from + toDragZone = to + } + } +} 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 55e9de5eff5f..ae1e4e0fbbc1 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 @@ -181,6 +181,17 @@ class DesktopModeCompatPolicyTest : ShellTestCase() { ) } + + @Test + @EnableFlags(Flags.FLAG_EXCLUDE_CAPTION_FROM_APP_BOUNDS) + @DisableCompatChanges(ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED) + @EnableCompatChanges(ActivityInfo.OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS) + fun testShouldExcludeCaptionFromAppBounds_resizeable_overridden_true() { + assertTrue(desktopModeCompatPolicy.shouldExcludeCaptionFromAppBounds( + setUpFreeformTask().apply { isResizeable = true }) + ) + } + fun setUpFreeformTask(): TaskInfo = createFreeformTask().apply { val componentName = 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 33f14acd0f02..391d46287498 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 @@ -157,33 +157,33 @@ class DesktopModeStatusTest : ShellTestCase() { } @Test - fun isDeviceEligibleForDesktopMode_configDEModeOn_returnsTrue() { - doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)) + fun isInternalDisplayEligibleToHostDesktops_configDEModeOn_returnsTrue() { + doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)) - assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isTrue() + assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isTrue() } @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @Test - fun isDeviceEligibleForDesktopMode_supportFlagOff_returnsFalse() { - assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse() + fun isInternalDisplayEligibleToHostDesktops_supportFlagOff_returnsFalse() { + assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isFalse() } @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @Test - fun isDeviceEligibleForDesktopMode_supportFlagOn_returnsFalse() { - assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse() + fun isInternalDisplayEligibleToHostDesktops_supportFlagOn_returnsFalse() { + assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isFalse() } @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @Test - fun isDeviceEligibleForDesktopMode_supportFlagOn_configDevOptModeOn_returnsTrue() { + fun isInternalDisplayEligibleToHostDesktops_supportFlagOn_configDevOptModeOn_returnsTrue() { doReturn(true).whenever(mockResources).getBoolean( eq(R.bool.config_isDesktopModeDevOptionSupported) ) - assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isTrue() + assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isTrue() } @DisableFlags(Flags.FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java index b9d6a454694d..e5a6a6d258dd 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java @@ -360,7 +360,8 @@ public class SplitTransitionTests extends ShellTestCase { mStageCoordinator.onRecentsInSplitAnimationFinishing(false /* returnToApp */, commitWCT, mock(SurfaceControl.Transaction.class)); } else { - mStageCoordinator.onRecentsInSplitAnimationFinish(commitWCT, + mStageCoordinator.onRecentsInSplitAnimationFinishing( + mStageCoordinator.wctIsReorderingSplitToTop(commitWCT), commitWCT, mock(SurfaceControl.Transaction.class)); } assertFalse(mStageCoordinator.isSplitScreenVisible()); @@ -430,7 +431,8 @@ public class SplitTransitionTests extends ShellTestCase { mStageCoordinator.onRecentsInSplitAnimationFinishing(true /* returnToApp */, restoreWCT, mock(SurfaceControl.Transaction.class)); } else { - mStageCoordinator.onRecentsInSplitAnimationFinish(restoreWCT, + mStageCoordinator.onRecentsInSplitAnimationFinishing( + mStageCoordinator.wctIsReorderingSplitToTop(restoreWCT), restoreWCT, mock(SurfaceControl.Transaction.class)); } assertTrue(mStageCoordinator.isSplitScreenVisible()); 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 53ae967e7bbf..067dcec5d65d 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 @@ -73,7 +73,7 @@ class DesktopModeWindowDecorViewModelAppHandleOnlyTest : .spyStatic(DesktopModeStatus::class.java) .spyStatic(DragPositioningCallbackUtility::class.java) .startMocking() - doReturn(false).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) } + doReturn(false).`when` { DesktopModeStatus.canEnterDesktopMode(any()) } doReturn(true).`when` { DesktopModeStatus.overridesShowAppHandle(any())} setUpCommon() whenever(mockDisplayController.getDisplay(anyInt())).thenReturn(mockDisplay) 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 f15418adf1e3..49812d381178 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,8 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest .spyStatic(DragPositioningCallbackUtility::class.java) .startMocking() - doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(Mockito.any()) } + doReturn(true).`when` { DesktopModeStatus.canInternalDisplayHostDesktops(Mockito.any()) } + doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(Mockito.any()) } doReturn(false).`when` { DesktopModeStatus.overridesShowAppHandle(Mockito.any()) } setUpCommon() @@ -384,7 +385,7 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true) val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN) - doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) } + doReturn(true).`when` { DesktopModeStatus.canInternalDisplayHostDesktops(any()) } setUpMockDecorationsForTasks(task) onTaskOpening(task) diff --git a/libs/hwui/OWNERS b/libs/hwui/OWNERS index bc174599a4d3..70d13ab8b3e5 100644 --- a/libs/hwui/OWNERS +++ b/libs/hwui/OWNERS @@ -4,7 +4,6 @@ alecmouri@google.com djsollen@google.com jreck@google.com njawad@google.com -scroggo@google.com sumir@google.com # For text, e.g. Typeface, Font, Minikin, etc. diff --git a/libs/hwui/hwui/Bitmap.cpp b/libs/hwui/hwui/Bitmap.cpp index b1550b0b6888..63a024b8e780 100644 --- a/libs/hwui/hwui/Bitmap.cpp +++ b/libs/hwui/hwui/Bitmap.cpp @@ -260,7 +260,7 @@ sk_sp<Bitmap> Bitmap::createFrom(AHardwareBuffer* hardwareBuffer, const SkImageI #endif sk_sp<Bitmap> Bitmap::createFrom(const SkImageInfo& info, size_t rowBytes, int fd, void* addr, - size_t size, bool readOnly) { + size_t size, bool readOnly, int64_t id) { #ifdef _WIN32 // ashmem not implemented on Windows return nullptr; #else @@ -279,7 +279,7 @@ sk_sp<Bitmap> Bitmap::createFrom(const SkImageInfo& info, size_t rowBytes, int f } } - sk_sp<Bitmap> bitmap(new Bitmap(addr, fd, size, info, rowBytes)); + sk_sp<Bitmap> bitmap(new Bitmap(addr, fd, size, info, rowBytes, id)); if (readOnly) { bitmap->setImmutable(); } @@ -334,7 +334,7 @@ Bitmap::Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info : SkPixelRef(info.width(), info.height(), address, rowBytes) , mInfo(validateAlpha(info)) , mPixelStorageType(PixelStorageType::Ashmem) - , mId(id != INVALID_BITMAP_ID ? id : getId(mPixelStorageType)) { + , mId(id != UNDEFINED_BITMAP_ID ? id : getId(mPixelStorageType)) { mPixelStorage.ashmem.address = address; mPixelStorage.ashmem.fd = fd; mPixelStorage.ashmem.size = mappedSize; diff --git a/libs/hwui/hwui/Bitmap.h b/libs/hwui/hwui/Bitmap.h index 8abe6a8c445a..4e9bcf27c0ef 100644 --- a/libs/hwui/hwui/Bitmap.h +++ b/libs/hwui/hwui/Bitmap.h @@ -97,7 +97,7 @@ public: BitmapPalette palette); #endif static sk_sp<Bitmap> createFrom(const SkImageInfo& info, size_t rowBytes, int fd, void* addr, - size_t size, bool readOnly); + size_t size, bool readOnly, int64_t id); static sk_sp<Bitmap> createFrom(const SkImageInfo&, SkPixelRef&); int rowBytesAsPixels() const { return rowBytes() >> mInfo.shiftPerPixel(); } @@ -183,15 +183,15 @@ public: static bool compress(const SkBitmap& bitmap, JavaCompressFormat format, int32_t quality, SkWStream* stream); -private: - static constexpr uint64_t INVALID_BITMAP_ID = 0u; + static constexpr uint64_t UNDEFINED_BITMAP_ID = 0u; +private: static sk_sp<Bitmap> allocateAshmemBitmap(size_t size, const SkImageInfo& i, size_t rowBytes); Bitmap(void* address, size_t allocSize, const SkImageInfo& info, size_t rowBytes); Bitmap(SkPixelRef& pixelRef, const SkImageInfo& info); Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info, size_t rowBytes, - uint64_t id = INVALID_BITMAP_ID); + uint64_t id = UNDEFINED_BITMAP_ID); #ifdef __ANDROID__ // Layoutlib does not support hardware acceleration Bitmap(AHardwareBuffer* buffer, const SkImageInfo& info, size_t rowBytes, BitmapPalette palette); diff --git a/libs/hwui/jni/Bitmap.cpp b/libs/hwui/jni/Bitmap.cpp index 29efd98b41d0..cfde0b28c0d5 100644 --- a/libs/hwui/jni/Bitmap.cpp +++ b/libs/hwui/jni/Bitmap.cpp @@ -191,9 +191,8 @@ void reinitBitmap(JNIEnv* env, jobject javaBitmap, const SkImageInfo& info, info.width(), info.height(), isPremultiplied); } -jobject createBitmap(JNIEnv* env, Bitmap* bitmap, - int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets, - int density) { +jobject createBitmap(JNIEnv* env, Bitmap* bitmap, int bitmapCreateFlags, jbyteArray ninePatchChunk, + jobject ninePatchInsets, int density, int64_t id) { static jmethodID gBitmap_constructorMethodID = GetMethodIDOrDie(env, gBitmap_class, "<init>", "(JJIIIZ[BLandroid/graphics/NinePatch$InsetStruct;Z)V"); @@ -208,10 +207,12 @@ jobject createBitmap(JNIEnv* env, Bitmap* bitmap, if (!isMutable) { bitmapWrapper->bitmap().setImmutable(); } + int64_t bitmapId = id != Bitmap::UNDEFINED_BITMAP_ID ? id : bitmap->getId(); jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID, - static_cast<jlong>(bitmap->getId()), reinterpret_cast<jlong>(bitmapWrapper), - bitmap->width(), bitmap->height(), density, - isPremultiplied, ninePatchChunk, ninePatchInsets, fromMalloc); + static_cast<jlong>(bitmapId), + reinterpret_cast<jlong>(bitmapWrapper), bitmap->width(), + bitmap->height(), density, isPremultiplied, ninePatchChunk, + ninePatchInsets, fromMalloc); if (env->ExceptionCheck() != 0) { ALOGE("*** Uncaught exception returned from Java call!\n"); @@ -759,6 +760,7 @@ static jobject Bitmap_createFromParcel(JNIEnv* env, jobject, jobject parcel) { const int32_t height = p.readInt32(); const int32_t rowBytes = p.readInt32(); const int32_t density = p.readInt32(); + const int64_t sourceId = p.readInt64(); if (kN32_SkColorType != colorType && kRGBA_F16_SkColorType != colorType && @@ -815,7 +817,8 @@ static jobject Bitmap_createFromParcel(JNIEnv* env, jobject, jobject parcel) { return STATUS_NO_MEMORY; } nativeBitmap = - Bitmap::createFrom(imageInfo, rowBytes, fd.release(), addr, size, !isMutable); + Bitmap::createFrom(imageInfo, rowBytes, fd.release(), addr, size, + !isMutable, sourceId); return STATUS_OK; }); @@ -831,15 +834,15 @@ static jobject Bitmap_createFromParcel(JNIEnv* env, jobject, jobject parcel) { } return createBitmap(env, nativeBitmap.release(), getPremulBitmapCreateFlags(isMutable), nullptr, - nullptr, density); + nullptr, density, sourceId); #else jniThrowRuntimeException(env, "Cannot use parcels outside of Android"); return NULL; #endif } -static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, - jlong bitmapHandle, jint density, jobject parcel) { +static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, jlong bitmapHandle, jint density, + jobject parcel) { #ifdef __ANDROID__ // Layoutlib does not support parcel if (parcel == NULL) { ALOGD("------- writeToParcel null parcel\n"); @@ -870,6 +873,7 @@ static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, binder_status_t status; int fd = bitmapWrapper->bitmap().getAshmemFd(); if (fd >= 0 && p.allowFds() && bitmap.isImmutable()) { + p.writeInt64(bitmapWrapper->bitmap().getId()); #if DEBUG_PARCEL ALOGD("Bitmap.writeToParcel: transferring immutable bitmap's ashmem fd as " "immutable blob (fds %s)", @@ -889,7 +893,7 @@ static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, ALOGD("Bitmap.writeToParcel: copying bitmap into new blob (fds %s)", p.allowFds() ? "allowed" : "forbidden"); #endif - + p.writeInt64(Bitmap::UNDEFINED_BITMAP_ID); status = writeBlob(p.get(), bitmapWrapper->bitmap().getId(), bitmap); if (status) { doThrowRE(env, "Could not copy bitmap to parcel blob."); diff --git a/libs/hwui/jni/Bitmap.h b/libs/hwui/jni/Bitmap.h index 21a93f066d9b..c93246a972b6 100644 --- a/libs/hwui/jni/Bitmap.h +++ b/libs/hwui/jni/Bitmap.h @@ -18,6 +18,7 @@ #include <jni.h> #include <android/bitmap.h> +#include <hwui/Bitmap.h> struct SkImageInfo; @@ -33,9 +34,9 @@ enum BitmapCreateFlags { kBitmapCreateFlag_Premultiplied = 0x2, }; -jobject createBitmap(JNIEnv* env, Bitmap* bitmap, - int bitmapCreateFlags, jbyteArray ninePatchChunk = nullptr, - jobject ninePatchInsets = nullptr, int density = -1); +jobject createBitmap(JNIEnv* env, Bitmap* bitmap, int bitmapCreateFlags, + jbyteArray ninePatchChunk = nullptr, jobject ninePatchInsets = nullptr, + int density = -1, int64_t id = Bitmap::UNDEFINED_BITMAP_ID); Bitmap& toBitmap(jlong bitmapHandle); diff --git a/libs/hwui/jni/ScopedParcel.cpp b/libs/hwui/jni/ScopedParcel.cpp index b0f5423813b7..95e4e01d8df8 100644 --- a/libs/hwui/jni/ScopedParcel.cpp +++ b/libs/hwui/jni/ScopedParcel.cpp @@ -39,6 +39,16 @@ uint32_t ScopedParcel::readUint32() { return temp; } +int64_t ScopedParcel::readInt64() { + int64_t temp = 0; + // TODO: This behavior-matches what android::Parcel does + // but this should probably be better + if (AParcel_readInt64(mParcel, &temp) != STATUS_OK) { + temp = 0; + } + return temp; +} + float ScopedParcel::readFloat() { float temp = 0.; if (AParcel_readFloat(mParcel, &temp) != STATUS_OK) { diff --git a/libs/hwui/jni/ScopedParcel.h b/libs/hwui/jni/ScopedParcel.h index fd8d6a210f0f..f2f138fda43c 100644 --- a/libs/hwui/jni/ScopedParcel.h +++ b/libs/hwui/jni/ScopedParcel.h @@ -35,12 +35,16 @@ public: uint32_t readUint32(); + int64_t readInt64(); + float readFloat(); void writeInt32(int32_t value) { AParcel_writeInt32(mParcel, value); } void writeUint32(uint32_t value) { AParcel_writeUint32(mParcel, value); } + void writeInt64(int64_t value) { AParcel_writeInt64(mParcel, value); } + void writeFloat(float value) { AParcel_writeFloat(mParcel, value); } bool allowFds() const { return AParcel_getAllowFds(mParcel); } diff --git a/libs/input/PointerControllerContext.cpp b/libs/input/PointerControllerContext.cpp index 747eb8e5ad1b..5406de8602d6 100644 --- a/libs/input/PointerControllerContext.cpp +++ b/libs/input/PointerControllerContext.cpp @@ -15,6 +15,7 @@ */ #include "PointerControllerContext.h" + #include "PointerController.h" namespace { @@ -184,7 +185,7 @@ void PointerControllerContext::PointerAnimator::handleVsyncEvents() { DisplayEventReceiver::Event buf[EVENT_BUFFER_SIZE]; while ((n = mDisplayEventReceiver.getEvents(buf, EVENT_BUFFER_SIZE)) > 0) { for (size_t i = 0; i < static_cast<size_t>(n); ++i) { - if (buf[i].header.type == DisplayEventReceiver::DISPLAY_EVENT_VSYNC) { + if (buf[i].header.type == DisplayEventType::DISPLAY_EVENT_VSYNC) { timestamp = buf[i].header.timestamp; gotVsync = true; } diff --git a/media/java/android/media/audiofx/HapticGenerator.java b/media/java/android/media/audiofx/HapticGenerator.java index d2523ef43b9e..7f94ddea9b84 100644 --- a/media/java/android/media/audiofx/HapticGenerator.java +++ b/media/java/android/media/audiofx/HapticGenerator.java @@ -36,6 +36,20 @@ import java.util.UUID; * <p>See {@link android.media.MediaPlayer#getAudioSessionId()} for details on audio sessions. * <p>See {@link android.media.audiofx.AudioEffect} class for more details on controlling audio * effects. + * + * <pre>{@code + * AudioManager audioManager = context.getSystemService(AudioManager.class); + * player = MediaPlayer.create( + * context, + * audioUri, + * new AudioAttributes.Builder().setHapticChannelsMuted(false).build(), + * audioManager.generateAudioSessionId() + * ); + * if (HapticGenerator.isAvailable()) { + * HapticGenerator.create(player.getAudioSessionId()).setEnabled(true); + * } + * player.start(); + * }</pre> */ public class HapticGenerator extends AudioEffect implements AutoCloseable { diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig index 2a6919c5e03d..0deed3982d9b 100644 --- a/media/java/android/media/flags/media_better_together.aconfig +++ b/media/java/android/media/flags/media_better_together.aconfig @@ -145,6 +145,16 @@ flag { } flag { + name: "enable_output_switcher_device_grouping" + namespace: "media_better_together" + description: "Enables selected items in Output Switcher to be grouped together." + bug: "388347018" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "enable_prevention_of_keep_alive_route_providers" namespace: "media_solutions" description: "Enables mechanisms to prevent route providers from keeping malicious apps alive." diff --git a/media/jni/android_media_MediaDrm.cpp b/media/jni/android_media_MediaDrm.cpp index 48cd53dc44d7..d50acd837bf9 100644 --- a/media/jni/android_media_MediaDrm.cpp +++ b/media/jni/android_media_MediaDrm.cpp @@ -27,7 +27,6 @@ #include "jni.h" #include <nativehelper/JNIHelp.h> -#include <android_companion_virtualdevice_flags.h> #include <android/companion/virtualnative/IVirtualDeviceManagerNative.h> #include <android/hardware/drm/1.3/IDrmFactory.h> #include <binder/Parcel.h> @@ -46,7 +45,6 @@ using ::android::companion::virtualnative::IVirtualDeviceManagerNative; using ::android::os::PersistableBundle; namespace drm = ::android::hardware::drm; -namespace virtualdevice_flags = android::companion::virtualdevice::flags; namespace android { @@ -1050,11 +1048,6 @@ DrmPlugin::SecurityLevel jintToSecurityLevel(jint jlevel) { } std::vector<int> getVirtualDeviceIds() { - if (!virtualdevice_flags::device_aware_drm()) { - ALOGW("Device-aware DRM flag disabled."); - return std::vector<int>(); - } - sp<IBinder> binder = defaultServiceManager()->checkService(String16("virtualdevice_native")); if (binder != nullptr) { diff --git a/packages/EasterEgg/AndroidManifest.xml b/packages/EasterEgg/AndroidManifest.xml index 96e5892f4d1d..bcc10ddde228 100644 --- a/packages/EasterEgg/AndroidManifest.xml +++ b/packages/EasterEgg/AndroidManifest.xml @@ -64,7 +64,7 @@ android:label="@string/u_egg_name" android:icon="@drawable/android16_patch_adaptive" android:configChanges="orientation|screenLayout|screenSize|density" - android:theme="@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen"> + android:theme="@style/Theme.Landroid"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.DEFAULT" /> diff --git a/packages/EasterEgg/res/drawable/ic_planet_large.xml b/packages/EasterEgg/res/drawable/ic_planet_large.xml new file mode 100644 index 000000000000..7ac7c38153f2 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_planet_large.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M12,12m-11,0a11,11 0,1 1,22 0a11,11 0,1 1,-22 0" + android:strokeWidth="2" + android:fillColor="#16161D" + android:strokeColor="#ffffff"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_planet_medium.xml b/packages/EasterEgg/res/drawable/ic_planet_medium.xml new file mode 100644 index 000000000000..e997b45eb6e5 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_planet_medium.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M12,12m-9,0a9,9 0,1 1,18 0a9,9 0,1 1,-18 0" + android:strokeWidth="2" + android:fillColor="#16161D" + android:strokeColor="#ffffff"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_planet_small.xml b/packages/EasterEgg/res/drawable/ic_planet_small.xml new file mode 100644 index 000000000000..43339573207b --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_planet_small.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M12,12m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0" + android:strokeWidth="2" + android:fillColor="#16161D" + android:strokeColor="#ffffff"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_planet_tiny.xml b/packages/EasterEgg/res/drawable/ic_planet_tiny.xml new file mode 100644 index 000000000000..c666765113da --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_planet_tiny.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M12,12m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0" + android:strokeWidth="2" + android:fillColor="#16161D" + android:strokeColor="#ffffff"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_spacecraft.xml b/packages/EasterEgg/res/drawable/ic_spacecraft.xml new file mode 100644 index 000000000000..3cef4ab29192 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_spacecraft.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" + android:width="24dp" + android:viewportHeight="24" android:viewportWidth="24" + > + <group android:translateX="10" android:translateY="12"> + <path + android:strokeColor="#FFFFFF" + android:strokeWidth="2" + android:pathData=" +M11.853 0 +C11.853 -4.418 8.374 -8 4.083 -8 +L-5.5 -8 +C-6.328 -8 -7 -7.328 -7 -6.5 +C-7 -5.672 -6.328 -5 -5.5 -5 +L-2.917 -5 +C-1.26 -5 0.083 -3.657 0.083 -2 +L0.083 2 +C0.083 3.657 -1.26 5 -2.917 5 +L-5.5 5 +C-6.328 5 -7 5.672 -7 6.5 +C-7 7.328 -6.328 8 -5.5 8 +L4.083 8 +C8.374 8 11.853 4.418 11.853 0 +Z + "/> + </group> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_spacecraft_filled.xml b/packages/EasterEgg/res/drawable/ic_spacecraft_filled.xml new file mode 100644 index 000000000000..7a0c70379f20 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_spacecraft_filled.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" + android:width="24dp" + android:viewportHeight="24" android:viewportWidth="24" + > + <group android:translateX="10" android:translateY="12"> + <path + android:strokeColor="#FFFFFF" + android:fillColor="#000000" + android:strokeWidth="2" + android:pathData=" +M11.853 0 +C11.853 -4.418 8.374 -8 4.083 -8 +L-5.5 -8 +C-6.328 -8 -7 -7.328 -7 -6.5 +C-7 -5.672 -6.328 -5 -5.5 -5 +L-2.917 -5 +C-1.26 -5 0.083 -3.657 0.083 -2 +L0.083 2 +C0.083 3.657 -1.26 5 -2.917 5 +L-5.5 5 +C-6.328 5 -7 5.672 -7 6.5 +C-7 7.328 -6.328 8 -5.5 8 +L4.083 8 +C8.374 8 11.853 4.418 11.853 0 +Z + "/> + </group> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_spacecraft_rotated.xml b/packages/EasterEgg/res/drawable/ic_spacecraft_rotated.xml new file mode 100644 index 000000000000..2d4ce106ef38 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_spacecraft_rotated.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +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. +--> +<rotate xmlns:android="http://schemas.android.com/apk/res/android" + android:drawable="@drawable/ic_spacecraft" + android:fromDegrees="0" + android:toDegrees="360" + />
\ No newline at end of file diff --git a/packages/EasterEgg/res/values/themes.xml b/packages/EasterEgg/res/values/themes.xml index 5b163043a356..3a87e456fc3b 100644 --- a/packages/EasterEgg/res/values/themes.xml +++ b/packages/EasterEgg/res/values/themes.xml @@ -1,7 +1,26 @@ -<resources> +<?xml version="1.0" encoding="utf-8"?><!-- +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. +--> +<resources> <style name="ThemeOverlay.EasterEgg.AppWidgetContainer" parent=""> <item name="appWidgetBackgroundColor">@color/light_blue_600</item> <item name="appWidgetTextColor">@color/light_blue_50</item> </style> -</resources>
\ No newline at end of file + + <style name="Theme.Landroid" parent="android:Theme.Material.NoActionBar"> + <item name="android:windowLightStatusBar">false</item> + <item name="android:windowLightNavigationBar">false</item> + </style> +</resources> diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt b/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt index fb5954ec9736..8214c540304e 100644 --- a/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt +++ b/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt @@ -41,14 +41,16 @@ class Autopilot(val ship: Spacecraft, val universe: Universe) : Entity { val telemetry: String get() = - listOf( - "---- AUTOPILOT ENGAGED ----", - "TGT: " + (target?.name?.toUpperCase() ?: "SELECTING..."), - "EXE: $strategy" + if (debug.isNotEmpty()) " ($debug)" else "", - ) - .joinToString("\n") - - private var strategy: String = "NONE" + if (enabled) + listOf( + "---- AUTOPILOT ENGAGED ----", + "TGT: " + (target?.name?.toUpperCase() ?: "SELECTING..."), + "EXE: $strategy" + if (debug.isNotEmpty()) " ($debug)" else "", + ) + .joinToString("\n") + else "" + + var strategy: String = "NONE" private var debug: String = "" override fun update(sim: Simulator, dt: Float) { @@ -119,7 +121,7 @@ class Autopilot(val ship: Spacecraft, val universe: Universe) : Entity { target.pos + Vec2.makeWithAngleMag( target.velocity.angle(), - min(altitude / 2, target.velocity.mag()) + min(altitude / 2, target.velocity.mag()), ) leadingVector = leadingPos - ship.pos diff --git a/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt b/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt index d040fba49fdf..e74863849efa 100644 --- a/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt +++ b/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt @@ -20,9 +20,19 @@ import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.Easing import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.material.minimumInteractiveComponentSize import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import kotlin.random.Random @Composable fun Dp.toLocalPx() = with(LocalDensity.current) { this@toLocalPx.toPx() } @@ -36,6 +46,40 @@ val flickerFadeIn = animationSpec = tween( durationMillis = 1000, - easing = CubicBezierEasing(0f, 1f, 1f, 0f) * flickerFadeEasing(Random) + easing = CubicBezierEasing(0f, 1f, 1f, 0f) * flickerFadeEasing(Random), ) ) + +fun flickerFadeInAfterDelay(delay: Int = 0) = + fadeIn( + animationSpec = + tween( + durationMillis = 1000, + delayMillis = delay, + easing = CubicBezierEasing(0f, 1f, 1f, 0f) * flickerFadeEasing(Random), + ) + ) + +@Composable +fun ConsoleButton( + modifier: Modifier = Modifier, + textStyle: TextStyle = TextStyle.Default, + color: Color, + bgColor: Color, + borderColor: Color, + text: String, + onClick: () -> Unit, +) { + Text( + style = textStyle, + color = color, + modifier = + modifier + .clickable { onClick() } + .background(color = bgColor) + .border(width = 1.dp, color = borderColor) + .padding(6.dp) + .minimumInteractiveComponentSize(), + text = text, + ) +} diff --git a/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt b/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt index d56e8b9e8d0e..8d4adf638bb3 100644 --- a/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt +++ b/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt @@ -56,6 +56,8 @@ class DreamUniverse : DreamService() { } } + private var notifier: UniverseProgressNotifier? = null + override fun onAttachedToWindow() { super.onAttachedToWindow() @@ -76,8 +78,8 @@ class DreamUniverse : DreamService() { Random.nextFloat() * PI2f, Random.nextFloatInRange( PLANET_ORBIT_RANGE.start, - PLANET_ORBIT_RANGE.endInclusive - ) + PLANET_ORBIT_RANGE.endInclusive, + ), ) } @@ -94,9 +96,11 @@ class DreamUniverse : DreamService() { composeView.setContent { Spaaaace(modifier = Modifier.fillMaxSize(), u = universe, foldState = foldState) DebugText(DEBUG_TEXT) - Telemetry(universe) + Telemetry(universe, showControls = false) } + notifier = UniverseProgressNotifier(this, universe) + composeView.setViewTreeLifecycleOwner(lifecycleOwner) composeView.setViewTreeSavedStateRegistryOwner(lifecycleOwner) diff --git a/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt b/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt index 4f77b00b7570..95a60c7a5292 100644 --- a/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt +++ b/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt @@ -21,6 +21,7 @@ import android.os.Build import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity +import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.animation.AnimatedVisibility @@ -34,6 +35,7 @@ import androidx.compose.foundation.gestures.transformable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -46,6 +48,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.currentRecomposeScope import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -59,6 +62,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.TextStyle @@ -74,9 +78,6 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.window.layout.FoldingFeature import androidx.window.layout.WindowInfoTracker -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import java.lang.Float.max import java.lang.Float.min import java.util.Calendar @@ -85,11 +86,14 @@ import kotlin.math.absoluteValue import kotlin.math.floor import kotlin.math.sqrt import kotlin.random.Random +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch enum class RandomSeedType { Fixed, Daily, - Evergreen + Evergreen, } const val TEST_UNIVERSE = false @@ -138,6 +142,10 @@ fun getDessertCode(): String = else -> Build.VERSION.RELEASE_OR_CODENAME.replace(Regex("[a-z]*"), "") } +fun getSystemDesignation(universe: Universe): String { + return "${getDessertCode()}-${universe.randomSeed % 100_000}" +} + val DEBUG_TEXT = mutableStateOf("Hello Universe") const val SHOW_DEBUG_TEXT = false @@ -150,13 +158,13 @@ fun DebugText(text: MutableState<String>) { fontWeight = FontWeight.Medium, fontSize = 9.sp, color = Color.Yellow, - text = text.value + text = text.value, ) } } @Composable -fun Telemetry(universe: Universe) { +fun Telemetry(universe: Universe, showControls: Boolean) { var topVisible by remember { mutableStateOf(false) } var bottomVisible by remember { mutableStateOf(false) } @@ -174,7 +182,6 @@ fun Telemetry(universe: Universe) { LaunchedEffect("blah") { delay(1000) bottomVisible = true - delay(1000) topVisible = true } @@ -183,13 +190,11 @@ fun Telemetry(universe: Universe) { // TODO: Narrow the scope of invalidation here to the specific data needed; // the behavior below mimics the previous implementation of a snapshot ticker value val recomposeScope = currentRecomposeScope - Telescope(universe) { - recomposeScope.invalidate() - } + Telescope(universe) { recomposeScope.invalidate() } BoxWithConstraints( modifier = - Modifier.fillMaxSize().padding(6.dp).windowInsetsPadding(WindowInsets.safeContent), + Modifier.fillMaxSize().padding(6.dp).windowInsetsPadding(WindowInsets.safeContent) ) { val wide = maxWidth > maxHeight Column( @@ -197,57 +202,82 @@ fun Telemetry(universe: Universe) { Modifier.align(if (wide) Alignment.BottomEnd else Alignment.BottomStart) .fillMaxWidth(if (wide) 0.45f else 1.0f) ) { - universe.ship.autopilot?.let { autopilot -> - if (autopilot.enabled) { + val autopilotEnabled = universe.ship.autopilot?.enabled == true + if (autopilotEnabled) { + universe.ship.autopilot?.let { autopilot -> AnimatedVisibility( modifier = Modifier, visible = bottomVisible, - enter = flickerFadeIn + enter = flickerFadeIn, ) { Text( style = textStyle, color = Colors.Autopilot, modifier = Modifier.align(Left), - text = autopilot.telemetry + text = autopilot.telemetry, ) } } } - AnimatedVisibility( - modifier = Modifier, - visible = bottomVisible, - enter = flickerFadeIn - ) { - Text( - style = textStyle, - color = Colors.Console, - modifier = Modifier.align(Left), - text = - with(universe.ship) { - val closest = universe.closestPlanet() - val distToClosest = ((closest.pos - pos).mag() - closest.radius).toInt() - listOfNotNull( - landing?.let { - "LND: ${it.planet.name.toUpperCase()}\nJOB: ${it.text}" - } - ?: if (distToClosest < 10_000) { - "ALT: $distToClosest" - } else null, - "THR: %.0f%%".format(thrust.mag() * 100f), - "POS: %s".format(pos.str("%+7.0f")), - "VEL: %.0f".format(velocity.mag()) - ) - .joinToString("\n") + Row(modifier = Modifier.padding(top = 6.dp)) { + AnimatedVisibility( + modifier = Modifier.weight(1f), + visible = bottomVisible, + enter = flickerFadeIn, + ) { + Text( + style = textStyle, + color = Colors.Console, + text = + with(universe.ship) { + val closest = universe.closestPlanet() + val distToClosest = + ((closest.pos - pos).mag() - closest.radius).toInt() + listOfNotNull( + landing?.let { + "LND: ${it.planet.name.toUpperCase()}\n" + + "JOB: ${it.text.toUpperCase()}" + } + ?: if (distToClosest < 10_000) { + "ALT: $distToClosest" + } else null, + "THR: %.0f%%".format(thrust.mag() * 100f), + "POS: %s".format(pos.str("%+7.0f")), + "VEL: %.0f".format(velocity.mag()), + ) + .joinToString("\n") + }, + ) + } + + if (showControls) { + AnimatedVisibility( + visible = bottomVisible, + enter = flickerFadeInAfterDelay(500), + ) { + ConsoleButton( + textStyle = textStyle, + color = Colors.Console, + bgColor = if (autopilotEnabled) Colors.Autopilot else Color.Transparent, + borderColor = Colors.Console, + text = "AUTO", + ) { + universe.ship.autopilot?.let { + it.enabled = !it.enabled + DYNAMIC_ZOOM = it.enabled + if (!it.enabled) universe.ship.thrust = Vec2.Zero + } } - ) + } + } } } AnimatedVisibility( modifier = Modifier.align(Alignment.TopStart), visible = topVisible, - enter = flickerFadeIn + enter = flickerFadeInAfterDelay(1000), ) { Text( style = textStyle, @@ -263,13 +293,12 @@ fun Telemetry(universe: Universe) { text = (with(universe.star) { listOf( - " STAR: $name (${getDessertCode()}-" + - "${universe.randomSeed % 100_000})", + " STAR: $name (${getSystemDesignation(universe)})", " CLASS: ${cls.name}", "RADIUS: ${radius.toInt()}", " MASS: %.3g".format(mass), "BODIES: ${explored.size} / ${universe.planets.size}", - "" + "", ) } + explored @@ -280,11 +309,11 @@ fun Telemetry(universe: Universe) { " ATMO: ${it.atmosphere.capitalize()}", " FAUNA: ${it.fauna.capitalize()}", " FLORA: ${it.flora.capitalize()}", - "" + "", ) } .flatten()) - .joinToString("\n") + .joinToString("\n"), // TODO: different colors, highlight latest discovery ) @@ -293,6 +322,7 @@ fun Telemetry(universe: Universe) { } class MainActivity : ComponentActivity() { + private var notifier: UniverseProgressNotifier? = null private var foldState = mutableStateOf<FoldingFeature?>(null) override fun onCreate(savedInstanceState: Bundle?) { @@ -300,7 +330,7 @@ class MainActivity : ComponentActivity() { onWindowLayoutInfoChange() - enableEdgeToEdge() + enableEdgeToEdge(statusBarStyle = SystemBarStyle.dark(Color.Red.toArgb())) val universe = Universe(namer = Namer(resources), randomSeed = randomSeed()) @@ -312,12 +342,13 @@ class MainActivity : ComponentActivity() { com.android.egg.ComponentActivationActivity.lockUnlockComponents(applicationContext) - // for autopilot testing in the activity - // val autopilot = Autopilot(universe.ship, universe) - // universe.ship.autopilot = autopilot - // universe.add(autopilot) - // autopilot.enabled = true - // DYNAMIC_ZOOM = autopilot.enabled + // set up the autopilot in case we need it + val autopilot = Autopilot(universe.ship, universe) + universe.ship.autopilot = autopilot + universe.add(autopilot) + autopilot.enabled = false + + notifier = UniverseProgressNotifier(this, universe) setContent { Spaaaace(modifier = Modifier.fillMaxSize(), u = universe, foldState = foldState) @@ -329,7 +360,7 @@ class MainActivity : ComponentActivity() { modifier = Modifier.fillMaxSize(), minRadius = minRadius, maxRadius = maxRadius, - color = Color.Green + color = Color.Green, ) { vec -> (universe.follow as? Spacecraft)?.let { ship -> if (vec == Vec2.Zero) { @@ -346,13 +377,13 @@ class MainActivity : ComponentActivity() { ship.thrust = Vec2.makeWithAngleMag( a, - lexp(minRadius, maxRadius, m).coerceIn(0f, 1f) + lexp(minRadius, maxRadius, m).coerceIn(0f, 1f), ) } } } } - Telemetry(universe) + Telemetry(universe, true) } } @@ -382,7 +413,7 @@ fun MainActivityPreview() { Spaaaace(modifier = Modifier.fillMaxSize(), universe) DebugText(DEBUG_TEXT) - Telemetry(universe) + Telemetry(universe, true) } @Composable @@ -391,7 +422,7 @@ fun FlightStick( minRadius: Float = 0f, maxRadius: Float = 1000f, color: Color = Color.Green, - onStickChanged: (vector: Vec2) -> Unit + onStickChanged: (vector: Vec2) -> Unit, ) { val origin = remember { mutableStateOf(Vec2.Zero) } val target = remember { mutableStateOf(Vec2.Zero) } @@ -444,14 +475,14 @@ fun FlightStick( PathEffect.dashPathEffect( floatArrayOf(this.density * 1f, this.density * 2f) ) - else null - ) + else null, + ), ) drawLine( color = color, start = origin.value, end = origin.value + Vec2.makeWithAngleMag(a, mag), - strokeWidth = 2f + strokeWidth = 2f, ) } } @@ -462,15 +493,13 @@ fun FlightStick( fun Spaaaace( modifier: Modifier, u: Universe, - foldState: MutableState<FoldingFeature?> = mutableStateOf(null) + foldState: MutableState<FoldingFeature?> = mutableStateOf(null), ) { LaunchedEffect(u) { - while (true) withInfiniteAnimationFrameNanos { frameTimeNanos -> - u.step(frameTimeNanos) - } + while (true) withInfiniteAnimationFrameNanos { frameTimeNanos -> u.step(frameTimeNanos) } } - var cameraZoom by remember { mutableStateOf(1f) } + var cameraZoom by remember { mutableFloatStateOf(DEFAULT_CAMERA_ZOOM) } var cameraOffset by remember { mutableStateOf(Offset.Zero) } val transformableState = @@ -501,15 +530,16 @@ fun Spaaaace( val closest = u.closestPlanet() val distToNearestSurf = max(0f, (u.ship.pos - closest.pos).mag() - closest.radius * 1.2f) // val normalizedDist = clamp(distToNearestSurf, 50f, 50_000f) / 50_000f - if (DYNAMIC_ZOOM) { - cameraZoom = - expSmooth( - cameraZoom, - clamp(500f / distToNearestSurf, MIN_CAMERA_ZOOM, MAX_CAMERA_ZOOM), - dt = u.dt, - speed = 1.5f - ) - } else if (!TOUCH_CAMERA_ZOOM) cameraZoom = DEFAULT_CAMERA_ZOOM + val targetZoom = + if (DYNAMIC_ZOOM) { + clamp(500f / distToNearestSurf, MIN_CAMERA_ZOOM, MAX_CAMERA_ZOOM) + } else { + DEFAULT_CAMERA_ZOOM + } + if (!TOUCH_CAMERA_ZOOM) { + cameraZoom = expSmooth(cameraZoom, targetZoom, dt = u.dt, speed = 1.5f) + } + if (!TOUCH_CAMERA_PAN) cameraOffset = (u.follow?.pos ?: Vec2.Zero) * -1f // cameraZoom: metersToPixels @@ -521,9 +551,9 @@ fun Spaaaace( -cameraOffset - Offset( visibleSpaceSizeMeters.width * centerFracX, - visibleSpaceSizeMeters.height * centerFracY + visibleSpaceSizeMeters.height * centerFracY, ), - visibleSpaceSizeMeters + visibleSpaceSizeMeters, ) var gridStep = 1000f @@ -537,14 +567,14 @@ fun Spaaaace( "fps: ${"%3.0f".format(1f / u.dt)} " + "dt: ${u.dt}\n" + ((u.follow as? Spacecraft)?.let { - "ship: p=%s v=%7.2f a=%6.3f t=%s\n".format( - it.pos.str("%+7.1f"), - it.velocity.mag(), - it.angle, - it.thrust.str("%+5.2f") - ) - } - ?: "") + + "ship: p=%s v=%7.2f a=%6.3f t=%s\n" + .format( + it.pos.str("%+7.1f"), + it.velocity.mag(), + it.angle, + it.thrust.str("%+5.2f"), + ) + } ?: "") + "star: '${u.star.name}' designation=UDC-${u.randomSeed % 100_000} " + "class=${u.star.cls.name} r=${u.star.radius.toInt()} m=${u.star.mass}\n" + "planets: ${u.planets.size}\n" + @@ -574,7 +604,7 @@ fun Spaaaace( translate( -visibleSpaceRectMeters.center.x + size.width * 0.5f, - -visibleSpaceRectMeters.center.y + size.height * 0.5f + -visibleSpaceRectMeters.center.y + size.height * 0.5f, ) { // debug outer frame // drawRect( @@ -590,7 +620,7 @@ fun Spaaaace( color = Colors.Eigengrau2, start = Offset(x, visibleSpaceRectMeters.top), end = Offset(x, visibleSpaceRectMeters.bottom), - strokeWidth = (if ((x % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom + strokeWidth = (if ((x % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom, ) x += gridStep } @@ -601,7 +631,7 @@ fun Spaaaace( color = Colors.Eigengrau2, start = Offset(visibleSpaceRectMeters.left, y), end = Offset(visibleSpaceRectMeters.right, y), - strokeWidth = (if ((y % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom + strokeWidth = (if ((y % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom, ) y += gridStep } diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt b/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt index 73318077f47a..babf1328c7d4 100644 --- a/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt +++ b/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt @@ -16,8 +16,8 @@ package com.android.egg.landroid -import android.content.res.Resources import com.android.egg.R +import android.content.res.Resources import kotlin.random.Random const val SUFFIX_PROB = 0.75f @@ -58,7 +58,7 @@ class Namer(resources: Resources) { 1f to "*", 1f to "^", 1f to "#", - 0.1f to "(^*!%@##!!" + 0.1f to "(^*!%@##!!", ) private var activities = Bag(resources.getStringArray(R.array.activities)) @@ -101,26 +101,26 @@ class Namer(resources: Resources) { fun floraPlural(rng: Random): String { return floraGenericPlurals.pull(rng) } + fun faunaPlural(rng: Random): String { return faunaGenericPlurals.pull(rng) } + fun atmoPlural(rng: Random): String { return atmoGenericPlurals.pull(rng) } val TEMPLATE_REGEX = Regex("""\{(flora|fauna|planet|atmo)\}""") + fun describeActivity(rng: Random, target: Planet?): String { - return activities - .pull(rng) - .replace(TEMPLATE_REGEX) { - when (it.groupValues[1]) { - "flora" -> (target?.flora ?: "SOME") + " " + floraPlural(rng) - "fauna" -> (target?.fauna ?: "SOME") + " " + faunaPlural(rng) - "atmo" -> (target?.atmosphere ?: "SOME") + " " + atmoPlural(rng) - "planet" -> (target?.description ?: "SOME BODY") // once told me - else -> "unknown template tag: ${it.groupValues[0]}" - } + return activities.pull(rng).replace(TEMPLATE_REGEX) { + when (it.groupValues[1]) { + "flora" -> (target?.flora ?: "SOME") + " " + floraPlural(rng) + "fauna" -> (target?.fauna ?: "SOME") + " " + faunaPlural(rng) + "atmo" -> (target?.atmosphere ?: "SOME") + " " + atmoPlural(rng) + "planet" -> (target?.description ?: "SOME BODY") // once told me + else -> "unknown template tag: ${it.groupValues[0]}" } - .toUpperCase() + } } } diff --git a/packages/EasterEgg/src/com/android/egg/landroid/UniverseProgressNotifier.kt b/packages/EasterEgg/src/com/android/egg/landroid/UniverseProgressNotifier.kt new file mode 100644 index 000000000000..bb3a04df6f36 --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/landroid/UniverseProgressNotifier.kt @@ -0,0 +1,187 @@ +/* + * 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.egg.landroid + +import com.android.egg.R + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.graphics.drawable.Icon +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.util.lerp +import kotlinx.coroutines.DisposableHandle + +const val CHANNEL_ID = "progress" +const val CHANNEL_NAME = "Spacecraft progress" +const val UPDATE_FREQUENCY_SEC = 1f + +fun lerpRange(range: ClosedFloatingPointRange<Float>, x: Float): Float = + lerp(range.start, range.endInclusive, x) + +class UniverseProgressNotifier(val context: Context, val universe: Universe) { + private val notificationId = universe.randomSeed.toInt() + private val chan = + NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT) + .apply { lockscreenVisibility = Notification.VISIBILITY_PUBLIC } + private val noman = + context.getSystemService(NotificationManager::class.java)?.apply { + createNotificationChannel(chan) + } + + private val registration: DisposableHandle = + universe.addSimulationStepListener(this::onSimulationStep) + + private val spacecraftIcon = Icon.createWithResource(context, R.drawable.ic_spacecraft_filled) + private val planetIcons = + listOf( + (lerpRange(PLANET_RADIUS_RANGE, 0.75f)) to + Icon.createWithResource(context, R.drawable.ic_planet_large), + (lerpRange(PLANET_RADIUS_RANGE, 0.5f)) to + Icon.createWithResource(context, R.drawable.ic_planet_medium), + (lerpRange(PLANET_RADIUS_RANGE, 0.25f)) to + Icon.createWithResource(context, R.drawable.ic_planet_small), + (PLANET_RADIUS_RANGE.start to + Icon.createWithResource(context, R.drawable.ic_planet_tiny)), + ) + + private fun getPlanetIcon(planet: Planet): Icon { + for ((radius, icon) in planetIcons) { + if (planet.radius > radius) return icon + } + return planetIcons.last().second + } + + private val progress = Notification.ProgressStyle().setProgressTrackerIcon(spacecraftIcon) + + private val builder = + Notification.Builder(context, CHANNEL_ID) + .setContentIntent( + PendingIntent.getActivity( + context, + 0, + Intent(context, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP + }, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + ) + .setPriority(Notification.PRIORITY_DEFAULT) + .setColorized(true) + .setOngoing(true) + .setColor(Colors.Eigengrau2.toArgb()) + .setStyle(progress) + + private var lastUpdate = 0f + private var initialDistToTarget = 0 + + private fun onSimulationStep() { + if (universe.now - lastUpdate >= UPDATE_FREQUENCY_SEC) { + lastUpdate = universe.now + // android.util.Log.v("Landroid", "posting notification at time ${universe.now}") + + var distToTarget = 0 + val autopilot = universe.ship.autopilot + val autopilotEnabled: Boolean = autopilot?.enabled == true + val target = autopilot?.target + val landing = universe.ship.landing + val speed = universe.ship.velocity.mag() + + if (landing != null) { + // landed + builder.setContentTitle("landed: ${landing.planet.name}") + builder.setContentText("currently: ${landing.text}") + builder.setShortCriticalText("landed") + + progress.setProgress(progress.progressMax) + progress.setProgressIndeterminate(false) + + builder.setStyle(progress) + } else if (autopilotEnabled) { + if (target != null) { + // autopilot en route + distToTarget = ((target.pos - universe.ship.pos).mag() - target.radius).toInt() + if (initialDistToTarget == 0) { + // we have a new target! + initialDistToTarget = distToTarget + progress.progressEndIcon = getPlanetIcon(target) + } + + val eta = if (speed > 0) "%1.0fs".format(distToTarget / speed) else "???" + builder.setContentTitle("headed to: ${target.name}") + builder.setContentText( + "autopilot is ${autopilot.strategy.toLowerCase()}" + + "\ndist: ${distToTarget}u // eta: $eta" + ) + // fun fact: ProgressStyle was originally EnRouteStyle + builder.setShortCriticalText("en route") + + progress + .setProgressSegments( + listOf( + Notification.ProgressStyle.Segment(initialDistToTarget) + .setColor(Colors.Track.toArgb()) + ) + ) + .setProgress(initialDistToTarget - distToTarget) + .setProgressIndeterminate(false) + builder.setStyle(progress) + } else { + // no target + if (initialDistToTarget != 0) { + // just launched + initialDistToTarget = 0 + progress.progressStartIcon = progress.progressEndIcon + progress.progressEndIcon = null + } + + builder.setContentTitle("in space") + builder.setContentText("selecting new target...") + builder.setShortCriticalText("launched") + + progress.setProgressIndeterminate(true) + + builder.setStyle(progress) + } + } else { + // under user control + + initialDistToTarget = 0 + + builder.setContentTitle("in space") + builder.setContentText("under manual control") + builder.setShortCriticalText("adrift") + + builder.setStyle(null) + } + + builder + .setSubText(getSystemDesignation(universe)) + .setSmallIcon(R.drawable.ic_spacecraft_rotated) + + val notification = builder.build() + + // one of the silliest things about Android is that icon levels go from 0 to 10000 + notification.iconLevel = (((universe.ship.angle + PI2f) / PI2f) * 10_000f).toInt() + + noman?.notify(notificationId, notification) + } + } +} diff --git a/packages/SettingsLib/Graph/graph.proto b/packages/SettingsLib/Graph/graph.proto index ec287c1b65b7..52a2160cdd74 100644 --- a/packages/SettingsLib/Graph/graph.proto +++ b/packages/SettingsLib/Graph/graph.proto @@ -95,6 +95,8 @@ message PreferenceProto { optional PermissionsProto write_permissions = 18; // Tag constants associated with the preference. repeated string tags = 19; + // Permit to read and write preference value (the lower 15 bits is reserved for read permit). + optional int32 read_write_permit = 20; // Target of an Intent message ActionTarget { diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt index e511bf1c175d..13541b1ebc9a 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt @@ -56,6 +56,8 @@ import com.android.settingslib.metadata.PreferenceScreenRegistry import com.android.settingslib.metadata.PreferenceSummaryProvider import com.android.settingslib.metadata.PreferenceTitleProvider import com.android.settingslib.metadata.ReadWritePermit +import com.android.settingslib.metadata.SensitivityLevel.Companion.HIGH_SENSITIVITY +import com.android.settingslib.metadata.SensitivityLevel.Companion.UNKNOWN_SENSITIVITY import com.android.settingslib.preference.PreferenceScreenFactory import com.android.settingslib.preference.PreferenceScreenProvider import java.util.Locale @@ -415,52 +417,46 @@ fun PreferenceMetadata.toProto( for (tag in metadata.tags(context)) addTags(tag) } persistent = metadata.isPersistent(context) - if (persistent) { - if (metadata is PersistentPreference<*>) { - sensitivityLevel = metadata.sensitivityLevel - metadata.getReadPermissions(context)?.let { - if (it.size > 0) readPermissions = it.toProto() - } - metadata.getWritePermissions(context)?.let { - if (it.size > 0) writePermissions = it.toProto() + if (metadata !is PersistentPreference<*>) return@preferenceProto + sensitivityLevel = metadata.sensitivityLevel + metadata.getReadPermissions(context)?.let { if (it.size > 0) readPermissions = it.toProto() } + metadata.getWritePermissions(context)?.let { if (it.size > 0) writePermissions = it.toProto() } + val readPermit = metadata.evalReadPermit(context, callingPid, callingUid) + val writePermit = + metadata.evalWritePermit(context, callingPid, callingUid) ?: ReadWritePermit.ALLOW + readWritePermit = ReadWritePermit.make(readPermit, writePermit) + if ( + flags.includeValue() && + enabled && + (!hasAvailable() || available) && + (!hasRestricted() || !restricted) && + readPermit == ReadWritePermit.ALLOW + ) { + val storage = metadata.storage(context) + value = preferenceValueProto { + when (metadata.valueType) { + Int::class.javaObjectType -> storage.getInt(metadata.key)?.let { intValue = it } + Boolean::class.javaObjectType -> + storage.getBoolean(metadata.key)?.let { booleanValue = it } + Float::class.javaObjectType -> + storage.getFloat(metadata.key)?.let { floatValue = it } + else -> {} } } - if ( - flags.includeValue() && - enabled && - (!hasAvailable() || available) && - (!hasRestricted() || !restricted) && - metadata is PersistentPreference<*> && - metadata.evalReadPermit(context, callingPid, callingUid) == ReadWritePermit.ALLOW - ) { - val storage = metadata.storage(context) - value = preferenceValueProto { - when (metadata.valueType) { - Int::class.javaObjectType -> storage.getInt(metadata.key)?.let { intValue = it } - Boolean::class.javaObjectType -> - storage.getBoolean(metadata.key)?.let { booleanValue = it } - Float::class.javaObjectType -> - storage.getFloat(metadata.key)?.let { floatValue = it } - else -> {} - } - } - } - if (flags.includeValueDescriptor()) { - valueDescriptor = preferenceValueDescriptorProto { - when (metadata) { - is IntRangeValuePreference -> rangeValue = rangeValueProto { - min = metadata.getMinValue(context) - max = metadata.getMaxValue(context) - step = metadata.getIncrementStep(context) - } - else -> {} - } - if (metadata is PersistentPreference<*>) { - when (metadata.valueType) { - Boolean::class.javaObjectType -> booleanType = true - Float::class.javaObjectType -> floatType = true + } + if (flags.includeValueDescriptor()) { + valueDescriptor = preferenceValueDescriptorProto { + when (metadata) { + is IntRangeValuePreference -> rangeValue = rangeValueProto { + min = metadata.getMinValue(context) + max = metadata.getMaxValue(context) + step = metadata.getIncrementStep(context) } - } + else -> {} + } + when (metadata.valueType) { + Boolean::class.javaObjectType -> booleanType = true + Float::class.javaObjectType -> floatType = true } } } @@ -478,6 +474,20 @@ fun <T> PersistentPreference<T>.evalReadPermit( else -> getReadPermit(context, callingPid, callingUid) } +/** Evaluates the write permit of a persistent preference. */ +fun <T> PersistentPreference<T>.evalWritePermit( + context: Context, + callingPid: Int, + callingUid: Int, +): Int? = + when { + sensitivityLevel == UNKNOWN_SENSITIVITY || sensitivityLevel == HIGH_SENSITIVITY -> + ReadWritePermit.DISALLOW + getWritePermissions(context)?.check(context, callingPid, callingUid) == false -> + ReadWritePermit.REQUIRE_APP_PERMISSION + else -> getWritePermit(context, callingPid, callingUid) + } + private fun PreferenceMetadata.getTitleTextProto(context: Context, isRoot: Boolean): TextProto? { if (isRoot && this is PreferenceScreenMetadata) { val titleRes = screenTitle diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt index 60f9c6bb92a3..72f6934b5f35 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt @@ -36,8 +36,6 @@ import com.android.settingslib.metadata.PreferenceRemoteOpMetricsLogger import com.android.settingslib.metadata.PreferenceRestrictionProvider import com.android.settingslib.metadata.PreferenceScreenRegistry import com.android.settingslib.metadata.ReadWritePermit -import com.android.settingslib.metadata.SensitivityLevel.Companion.HIGH_SENSITIVITY -import com.android.settingslib.metadata.SensitivityLevel.Companion.UNKNOWN_SENSITIVITY /** Request to set preference value. */ class PreferenceSetterRequest( @@ -223,13 +221,8 @@ fun <T> PersistentPreference<T>.evalWritePermit( callingPid: Int, callingUid: Int, ): Int = - when { - sensitivityLevel == UNKNOWN_SENSITIVITY || sensitivityLevel == HIGH_SENSITIVITY -> - ReadWritePermit.DISALLOW - getWritePermissions(context)?.check(context, callingPid, callingUid) == false -> - ReadWritePermit.REQUIRE_APP_PERMISSION - else -> getWritePermit(context, value, callingPid, callingUid) - } + evalWritePermit(context, callingPid, callingUid) + ?: getWritePermit(context, value, callingPid, callingUid) /** Message codec for [PreferenceSetterRequest]. */ object PreferenceSetterRequestCodec : MessageCodec<PreferenceSetterRequest> { diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt index e456a7f1aa1c..c723dce82b5a 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt @@ -41,6 +41,19 @@ annotation class ReadWritePermit { const val REQUIRE_APP_PERMISSION = 2 /** Require explicit user agreement (e.g. terms of service). */ const val REQUIRE_USER_AGREEMENT = 3 + + private const val READ_PERMIT_BITS = 15 + private const val READ_PERMIT_MASK = (1 shl 16) - 1 + + /** Wraps given read and write permit into an integer. */ + fun make(readPermit: @ReadWritePermit Int, writePermit: @ReadWritePermit Int): Int = + (writePermit shl READ_PERMIT_BITS) or readPermit + + /** Extracts the read permit from given integer generated by [make]. */ + fun getReadPermit(readWritePermit: Int): Int = readWritePermit and READ_PERMIT_MASK + + /** Extracts the write permit from given integer generated by [make]. */ + fun getWritePermit(readWritePermit: Int): Int = readWritePermit shr READ_PERMIT_BITS } } @@ -81,6 +94,12 @@ interface PersistentPreference<T> : PreferenceMetadata { /** The value type the preference is associated with. */ val valueType: Class<T> + /** The sensitivity level of the preference. */ + val sensitivityLevel: @SensitivityLevel Int + get() = SensitivityLevel.UNKNOWN_SENSITIVITY + + override fun isPersistent(context: Context) = true + /** * Returns the key-value storage of the preference. * @@ -102,19 +121,27 @@ interface PersistentPreference<T> : PreferenceMetadata { * behind the scene. */ fun getReadPermit(context: Context, callingPid: Int, callingUid: Int): @ReadWritePermit Int = - PreferenceScreenRegistry.getReadPermit( - context, - callingPid, - callingUid, - this, - ) + PreferenceScreenRegistry.defaultReadPermit /** Returns the required permissions to write preference value. */ fun getWritePermissions(context: Context): Permissions? = null /** * Returns if the external application (identified by [callingPid] and [callingUid]) is - * permitted to write preference with given [value]. + * permitted to write preference value. If the write permit depends on certain value, implement + * the overloading [getWritePermit] instead. + * + * The underlying implementation does NOT need to check common states like isEnabled, + * isRestricted, isAvailable or permissions in [getWritePermissions]. The framework will do it + * behind the scene. + */ + fun getWritePermit(context: Context, callingPid: Int, callingUid: Int): @ReadWritePermit Int? = + null + + /** + * Returns if the external application (identified by [callingPid] and [callingUid]) is + * permitted to write preference with given [value]. Note that if the overloading + * [getWritePermit] returns non null value, this method will be ignored! * * The underlying implementation does NOT need to check common states like isEnabled, * isRestricted, isAvailable or permissions in [getWritePermissions]. The framework will do it @@ -125,18 +152,7 @@ interface PersistentPreference<T> : PreferenceMetadata { value: T?, callingPid: Int, callingUid: Int, - ): @ReadWritePermit Int = - PreferenceScreenRegistry.getWritePermit( - context, - value, - callingPid, - callingUid, - this, - ) - - /** The sensitivity level of the preference. */ - val sensitivityLevel: @SensitivityLevel Int - get() = SensitivityLevel.UNKNOWN_SENSITIVITY + ): @ReadWritePermit Int = PreferenceScreenRegistry.defaultWritePermit } /** Descriptor of values. */ diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt index a8939ab0d902..7f2a61081fbb 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt @@ -127,7 +127,7 @@ interface PreferenceMetadata { fun dependencies(context: Context): Array<String> = arrayOf() /** Returns if the preference is persistent in datastore. */ - fun isPersistent(context: Context): Boolean = this is PersistentPreference<*> + fun isPersistent(context: Context): Boolean = false /** * Returns if preference value backup is allowed (by default returns `true` if preference is diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt index 246310984db9..8d4bfffb1fdb 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt @@ -22,12 +22,18 @@ import android.util.Log import com.android.settingslib.datastore.KeyValueStore /** Registry of all available preference screens in the app. */ -object PreferenceScreenRegistry : ReadWritePermitProvider { +object PreferenceScreenRegistry { private const val TAG = "ScreenRegistry" /** Provider of key-value store. */ private lateinit var keyValueStoreProvider: KeyValueStoreProvider + /** The default permit for external application to read preference values. */ + var defaultReadPermit: @ReadWritePermit Int = ReadWritePermit.DISALLOW + + /** The default permit for external application to write preference values. */ + var defaultWritePermit: @ReadWritePermit Int = ReadWritePermit.DISALLOW + /** * Factories of all available [PreferenceScreenMetadata]s. * @@ -38,9 +44,6 @@ object PreferenceScreenRegistry : ReadWritePermitProvider { /** Metrics logger for preference actions triggered by user interaction. */ var preferenceUiActionMetricsLogger: PreferenceUiActionMetricsLogger? = null - private var readWritePermitProvider: ReadWritePermitProvider = - object : ReadWritePermitProvider {} - /** Sets the [KeyValueStoreProvider]. */ fun setKeyValueStoreProvider(keyValueStoreProvider: KeyValueStoreProvider) { this.keyValueStoreProvider = keyValueStoreProvider @@ -77,28 +80,6 @@ object PreferenceScreenRegistry : ReadWritePermitProvider { return null } } - - /** - * Sets the provider to check read write permit. Read and write requests are denied by default. - */ - fun setReadWritePermitProvider(readWritePermitProvider: ReadWritePermitProvider) { - this.readWritePermitProvider = readWritePermitProvider - } - - override fun getReadPermit( - context: Context, - callingPid: Int, - callingUid: Int, - preference: PreferenceMetadata, - ) = readWritePermitProvider.getReadPermit(context, callingPid, callingUid, preference) - - override fun getWritePermit( - context: Context, - value: Any?, - callingPid: Int, - callingUid: Int, - preference: PreferenceMetadata, - ) = readWritePermitProvider.getWritePermit(context, value, callingPid, callingUid, preference) } /** Provider of [KeyValueStore]. */ @@ -113,25 +94,3 @@ fun interface KeyValueStoreProvider { */ fun getKeyValueStore(context: Context, preference: PreferenceMetadata): KeyValueStore? } - -/** Provider of read and write permit. */ -interface ReadWritePermitProvider { - - val defaultReadWritePermit: @ReadWritePermit Int - get() = ReadWritePermit.DISALLOW - - fun getReadPermit( - context: Context, - callingPid: Int, - callingUid: Int, - preference: PreferenceMetadata, - ): @ReadWritePermit Int = defaultReadWritePermit - - fun getWritePermit( - context: Context, - value: Any?, - callingPid: Int, - callingUid: Int, - preference: PreferenceMetadata, - ): @ReadWritePermit Int = defaultReadWritePermit -} diff --git a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java index e173c5e996df..0f6a2a082e0c 100644 --- a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java +++ b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java @@ -118,6 +118,7 @@ public class SettingsSpinnerPreference extends Preference spinner.setAdapter(mAdapter); spinner.setSelection(mPosition); spinner.setOnItemSelectedListener(mOnSelectedListener); + spinner.setLongClickable(false); if (mShouldPerformClick) { mShouldPerformClick = false; // To show dropdown view. diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt index 0c7d6f093289..b173db0a0505 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt @@ -37,7 +37,7 @@ object BluetoothLeBroadcastMetadataExt { private const val KEY_BT_ADVERTISER_ADDRESS = "AD" private const val KEY_BT_BROADCAST_ID = "BI" private const val KEY_BT_BROADCAST_CODE = "BC" - private const val KEY_BT_STREAM_METADATA = "MD" + private const val KEY_BT_PUBLIC_METADATA = "PM" private const val KEY_BT_STANDARD_QUALITY = "SQ" private const val KEY_BT_HIGH_QUALITY = "HQ" @@ -84,7 +84,7 @@ object BluetoothLeBroadcastMetadataExt { } if (this.publicBroadcastMetadata != null && this.publicBroadcastMetadata?.rawMetadata?.size != 0) { - entries.add(Pair(KEY_BT_STREAM_METADATA, Base64.encodeToString( + entries.add(Pair(KEY_BT_PUBLIC_METADATA, Base64.encodeToString( this.publicBroadcastMetadata?.rawMetadata, Base64.NO_WRAP))) } if ((this.audioConfigQuality and @@ -160,7 +160,7 @@ object BluetoothLeBroadcastMetadataExt { var sourceAdvertiserSid = -1 var broadcastId = -1 var broadcastName: String? = null - var streamMetadata: BluetoothLeAudioContentMetadata? = null + var publicMetadata: BluetoothLeAudioContentMetadata? = null var paSyncInterval = -1 var broadcastCode: ByteArray? = null var audioConfigQualityStandard = -1 @@ -207,11 +207,11 @@ object BluetoothLeBroadcastMetadataExt { broadcastCode = Base64.decode(value.dropLastWhile { it.equals(0.toByte()) } .toByteArray(), Base64.NO_WRAP) } - KEY_BT_STREAM_METADATA -> { - require(streamMetadata == null) { - "Duplicate streamMetadata $input" + KEY_BT_PUBLIC_METADATA -> { + require(publicMetadata == null) { + "Duplicate publicMetadata $input" } - streamMetadata = BluetoothLeAudioContentMetadata + publicMetadata = BluetoothLeAudioContentMetadata .fromRawBytes(Base64.decode(value, Base64.NO_WRAP)) } KEY_BT_STANDARD_QUALITY -> { @@ -256,7 +256,7 @@ object BluetoothLeBroadcastMetadataExt { Log.d(TAG, "parseQrCodeToMetadata: main data elements sourceAddrType=$sourceAddrType, " + "sourceAddr=$sourceAddrString, sourceAdvertiserSid=$sourceAdvertiserSid, " + "broadcastId=$broadcastId, broadcastName=$broadcastName, " + - "streamMetadata=${streamMetadata != null}, " + + "publicMetadata=${publicMetadata != null}, " + "paSyncInterval=$paSyncInterval, " + "broadcastCode=${broadcastCode?.toString(Charsets.UTF_8)}, " + "audioConfigQualityStandard=$audioConfigQualityStandard, " + @@ -317,7 +317,7 @@ object BluetoothLeBroadcastMetadataExt { setBroadcastName(broadcastName) // QR code should set PBP(public broadcast profile) for auracast setPublicBroadcast(true) - setPublicBroadcastMetadata(streamMetadata) + setPublicBroadcastMetadata(publicMetadata) setPaSyncInterval(paSyncInterval) setEncrypted(broadcastCode != null) setBroadcastCode(broadcastCode) diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java index bf86911ee683..572444edea29 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java @@ -30,11 +30,13 @@ import android.util.Log; import androidx.annotation.ChecksSdkIntAtLeast; import com.android.internal.annotations.VisibleForTesting; +import com.android.settingslib.flags.Flags; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -385,7 +387,7 @@ public class CsipDeviceManager { preferredMainDevice.refresh(); hasChanged = true; } - syncAudioSharingSourceIfNeeded(preferredMainDevice); + syncAudioSharingStatusIfNeeded(preferredMainDevice); } if (hasChanged) { log("addMemberDevicesIntoMainDevice: After changed, CachedBluetoothDevice list: " @@ -399,13 +401,16 @@ public class CsipDeviceManager { return userManager != null && userManager.isManagedProfile(); } - private void syncAudioSharingSourceIfNeeded(CachedBluetoothDevice mainDevice) { + private void syncAudioSharingStatusIfNeeded(CachedBluetoothDevice mainDevice) { boolean isAudioSharingEnabled = BluetoothUtils.isAudioSharingUIAvailable(mContext); - if (isAudioSharingEnabled) { + if (isAudioSharingEnabled && mainDevice != null) { if (isWorkProfile()) { - log("addMemberDevicesIntoMainDevice: skip sync source for work profile"); + log("addMemberDevicesIntoMainDevice: skip sync audio sharing status, work profile"); return; } + Set<CachedBluetoothDevice> deviceSet = new HashSet<>(); + deviceSet.add(mainDevice); + deviceSet.addAll(mainDevice.getMemberDevice()); boolean hasBroadcastSource = BluetoothUtils.isBroadcasting(mBtManager) && BluetoothUtils.hasConnectedBroadcastSource( mainDevice, mBtManager); @@ -419,9 +424,6 @@ public class CsipDeviceManager { if (metadata != null && assistant != null) { log("addMemberDevicesIntoMainDevice: sync audio sharing source after " + "combining the top level devices."); - Set<CachedBluetoothDevice> deviceSet = new HashSet<>(); - deviceSet.add(mainDevice); - deviceSet.addAll(mainDevice.getMemberDevice()); Set<BluetoothDevice> sinksToSync = deviceSet.stream() .map(CachedBluetoothDevice::getDevice) .filter(device -> @@ -435,8 +437,24 @@ public class CsipDeviceManager { } } } + if (Flags.enableTemporaryBondDevicesUi()) { + log("addMemberDevicesIntoMainDevice: sync temp bond metadata for audio sharing " + + "sinks after combining the top level devices."); + Set<BluetoothDevice> sinksToSync = deviceSet.stream() + .map(CachedBluetoothDevice::getDevice).filter(Objects::nonNull).collect( + Collectors.toSet()); + if (sinksToSync.stream().anyMatch(BluetoothUtils::isTemporaryBondDevice)) { + for (BluetoothDevice device : sinksToSync) { + if (!BluetoothUtils.isTemporaryBondDevice(device)) { + log("addMemberDevicesIntoMainDevice: sync temp bond metadata for " + + device.getAnonymizedAddress()); + BluetoothUtils.setTemporaryBondMetadata(device); + } + } + } + } } else { - log("addMemberDevicesIntoMainDevice: skip sync source, flag disabled"); + log("addMemberDevicesIntoMainDevice: skip sync audio sharing status, flag disabled"); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java index 69e41a36f48f..aa84571c73d0 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java @@ -779,7 +779,7 @@ public abstract class InfoMediaManager { static List<RouteListingPreference.Item> composePreferenceRouteListing( RouteListingPreference routeListingPreference) { boolean preferRouteListingOrdering = - com.android.media.flags.Flags.enableOutputSwitcherSessionGrouping() + com.android.media.flags.Flags.enableOutputSwitcherDeviceGrouping() && preferRouteListingOrdering(routeListingPreference); List<RouteListingPreference.Item> finalizedItemList = new ArrayList<>(); List<RouteListingPreference.Item> itemList = routeListingPreference.getItems(); @@ -814,7 +814,7 @@ public abstract class InfoMediaManager { * Returns an ordered list of available devices based on the provided {@code * routeListingPreferenceItems}. * - * <p>The resulting order if enableOutputSwitcherSessionGrouping is disabled is: + * <p>The resulting order if enableOutputSwitcherDeviceGrouping is disabled is: * * <ol> * <li>Selected routes. @@ -822,7 +822,7 @@ public abstract class InfoMediaManager { * <li>Not-selected, non-system, available routes sorted by route listing preference. * </ol> * - * <p>The resulting order if enableOutputSwitcherSessionGrouping is enabled is: + * <p>The resulting order if enableOutputSwitcherDeviceGrouping is enabled is: * * <ol> * <li>Selected routes sorted by route listing preference. @@ -848,7 +848,7 @@ public abstract class InfoMediaManager { Set<String> sortedRouteIds = new LinkedHashSet<>(); boolean addSelectedRlpItemsFirst = - com.android.media.flags.Flags.enableOutputSwitcherSessionGrouping() + com.android.media.flags.Flags.enableOutputSwitcherDeviceGrouping() && preferRouteListingOrdering(routeListingPreference); Set<String> selectedRouteIds = new HashSet<>(); diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.kt b/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.kt index 10156c404ebf..bac564c7d0f4 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.kt @@ -20,6 +20,7 @@ import android.content.Intent import android.content.pm.PackageManager import android.media.MediaMetadata import android.media.session.MediaController +import android.media.session.MediaController.PlaybackInfo import android.media.session.MediaSession import android.media.session.MediaSessionManager import android.media.session.PlaybackState @@ -98,16 +99,22 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) { } /** Set volume `level` to remote media `token` */ - fun setVolume(token: MediaSession.Token, level: Int) { + fun setVolume(sessionId: SessionId, volumeLevel: Int) { + when (sessionId) { + is SessionId.Media -> setMediaSessionVolume(sessionId.token, volumeLevel) + } + } + + private fun setMediaSessionVolume(token: MediaSession.Token, volumeLevel: Int) { val record = mRecords[token] if (record == null) { Log.w(TAG, "setVolume: No record found for token $token") return } if (D.BUG) { - Log.d(TAG, "Setting level to $level") + Log.d(TAG, "Setting level to $volumeLevel") } - record.controller.setVolumeTo(level, 0) + record.controller.setVolumeTo(volumeLevel, 0) } private fun onRemoteVolumeChangedH(sessionToken: MediaSession.Token, flags: Int) { @@ -122,7 +129,7 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) { ) } val token = controller.sessionToken - mCallbacks.onRemoteVolumeChanged(token, flags) + mCallbacks.onRemoteVolumeChanged(SessionId.from(token), flags) } private fun onUpdateRemoteSessionListH(sessionToken: MediaSession.Token?) { @@ -158,7 +165,7 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) { controller.registerCallback(record, mHandler) } val record = mRecords[token] - val remote = isRemote(playbackInfo) + val remote = playbackInfo.isRemote() if (remote) { updateRemoteH(token, record!!.name, playbackInfo) record.sentRemote = true @@ -172,7 +179,7 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) { Log.d(TAG, "Removing " + record.name + " sentRemote=" + record.sentRemote) } if (record.sentRemote) { - mCallbacks.onRemoteRemoved(token) + mCallbacks.onRemoteRemoved(SessionId.from(token)) record.sentRemote = false } } @@ -213,8 +220,8 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) { private fun updateRemoteH( token: MediaSession.Token, name: String?, - pi: MediaController.PlaybackInfo, - ) = mCallbacks.onRemoteUpdate(token, name, pi) + playbackInfo: PlaybackInfo, + ) = mCallbacks.onRemoteUpdate(SessionId.from(token), name, VolumeInfo.from(playbackInfo)) private inner class MediaControllerRecord(val controller: MediaController) : MediaController.Callback() { @@ -225,7 +232,7 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) { return method + " " + controller.packageName + " " } - override fun onAudioInfoChanged(info: MediaController.PlaybackInfo) { + override fun onAudioInfoChanged(info: PlaybackInfo) { if (D.BUG) { Log.d( TAG, @@ -235,9 +242,9 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) { sentRemote), ) } - val remote = isRemote(info) + val remote = info.isRemote() if (!remote && sentRemote) { - mCallbacks.onRemoteRemoved(controller.sessionToken) + mCallbacks.onRemoteRemoved(SessionId.from(controller.sessionToken)) sentRemote = false } else if (remote) { updateRemoteH(controller.sessionToken, name, info) @@ -301,20 +308,36 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) { } } + /** Opaque id for ongoing sessions that support volume adjustment. */ + sealed interface SessionId { + + companion object { + fun from(token: MediaSession.Token) = Media(token) + } + + data class Media(val token: MediaSession.Token) : SessionId + } + + /** Holds session volume information. */ + data class VolumeInfo(val currentVolume: Int, val maxVolume: Int) { + + companion object { + + fun from(playbackInfo: PlaybackInfo) = + VolumeInfo(playbackInfo.currentVolume, playbackInfo.maxVolume) + } + } + /** Callback for remote media sessions */ interface Callbacks { /** Invoked when remote media session is updated */ - fun onRemoteUpdate( - token: MediaSession.Token?, - name: String?, - pi: MediaController.PlaybackInfo?, - ) + fun onRemoteUpdate(token: SessionId?, name: String?, volumeInfo: VolumeInfo?) /** Invoked when remote media session is removed */ - fun onRemoteRemoved(token: MediaSession.Token?) + fun onRemoteRemoved(token: SessionId?) /** Invoked when remote volume is changed */ - fun onRemoteVolumeChanged(token: MediaSession.Token?, flags: Int) + fun onRemoteVolumeChanged(token: SessionId?, flags: Int) } companion object { @@ -325,12 +348,11 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) { const val UPDATE_REMOTE_SESSION_LIST: Int = 3 private const val USE_SERVICE_LABEL = false - - private fun isRemote(pi: MediaController.PlaybackInfo?): Boolean = - pi != null && pi.playbackType == MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE } } +private fun PlaybackInfo?.isRemote() = this?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE + private fun MediaController.dump(n: Int, writer: PrintWriter) { writer.println(" Controller $n: $packageName") diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java index fd14d1ff6786..2eccaa626f3b 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java @@ -40,6 +40,8 @@ import android.content.Context; import android.os.Looper; import android.os.Parcel; import android.os.UserManager; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import com.android.settingslib.flags.Flags; @@ -74,6 +76,9 @@ public class CsipDeviceManagerTest { private final static String DEVICE_ADDRESS_1 = "AA:BB:CC:DD:EE:11"; private final static String DEVICE_ADDRESS_2 = "AA:BB:CC:DD:EE:22"; private final static String DEVICE_ADDRESS_3 = "AA:BB:CC:DD:EE:33"; + private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25; + private static final String TEMP_BOND_METADATA = + "<TEMP_BOND_TYPE>le_audio_sharing</TEMP_BOND_TYPE>"; private final static int GROUP1 = 1; private final BluetoothClass DEVICE_CLASS_1 = createBtClass(BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES); @@ -337,6 +342,7 @@ public class CsipDeviceManagerTest { } @Test + @DisableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_returnTrue() { // Condition: The preferredDevice is main and there is another main device in top list // Expected Result: return true and there is the preferredDevice in top list @@ -346,7 +352,6 @@ public class CsipDeviceManagerTest { mCachedDevices.add(preferredDevice); mCachedDevices.add(mCachedDevice2); mCachedDevices.add(mCachedDevice3); - mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) .isTrue(); @@ -359,6 +364,7 @@ public class CsipDeviceManagerTest { } @Test + @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_workProfile_doNothing() { // Condition: The preferredDevice is main and there is another main device in top list @@ -369,7 +375,6 @@ public class CsipDeviceManagerTest { mCachedDevices.add(preferredDevice); mCachedDevices.add(mCachedDevice2); mCachedDevices.add(mCachedDevice3); - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); when(mBroadcast.isEnabled(null)).thenReturn(true); BluetoothLeBroadcastMetadata metadata = Mockito.mock(BluetoothLeBroadcastMetadata.class); when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(metadata); @@ -377,6 +382,8 @@ public class CsipDeviceManagerTest { BluetoothLeBroadcastReceiveState.class); when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L)); when(mAssistant.getAllSources(mDevice2)).thenReturn(ImmutableList.of(state)); + when(mDevice2.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) + .thenReturn(TEMP_BOND_METADATA.getBytes()); when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager); when(mUserManager.isManagedProfile()).thenReturn(true); @@ -387,10 +394,13 @@ public class CsipDeviceManagerTest { assertThat(mCachedDevices.contains(mCachedDevice3)).isTrue(); assertThat(preferredDevice.getMemberDevice()).contains(mCachedDevice2); verify(mAssistant, never()).addSource(mDevice1, metadata, /* isGroupOp= */ false); + verify(mDevice1, never()).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, + TEMP_BOND_METADATA.getBytes()); } @Test - public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_syncSource() { + @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) + public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_syncState() { // Condition: The preferredDevice is main and there is another main device in top list // Expected Result: return true and there is the preferredDevice in top list CachedBluetoothDevice preferredDevice = mCachedDevice1; @@ -399,7 +409,6 @@ public class CsipDeviceManagerTest { mCachedDevices.add(preferredDevice); mCachedDevices.add(mCachedDevice2); mCachedDevices.add(mCachedDevice3); - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); when(mBroadcast.isEnabled(null)).thenReturn(true); BluetoothLeBroadcastMetadata metadata = Mockito.mock(BluetoothLeBroadcastMetadata.class); when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(metadata); @@ -407,6 +416,8 @@ public class CsipDeviceManagerTest { BluetoothLeBroadcastReceiveState.class); when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L)); when(mAssistant.getAllSources(mDevice2)).thenReturn(ImmutableList.of(state)); + when(mDevice2.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) + .thenReturn(TEMP_BOND_METADATA.getBytes()); assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) .isTrue(); @@ -415,6 +426,8 @@ public class CsipDeviceManagerTest { assertThat(mCachedDevices.contains(mCachedDevice3)).isTrue(); assertThat(preferredDevice.getMemberDevice()).contains(mCachedDevice2); verify(mAssistant).addSource(mDevice1, metadata, /* isGroupOp= */ false); + verify(mDevice1).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, + TEMP_BOND_METADATA.getBytes()); } @Test @@ -436,13 +449,13 @@ public class CsipDeviceManagerTest { } @Test + @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) public void addMemberDevicesIntoMainDevice_preferredDeviceIsMemberAndTwoMain_returnTrue() { // Condition: The preferredDevice is member and there are two main device in top list // Expected Result: return true and there is the preferredDevice in top list CachedBluetoothDevice preferredDevice = mCachedDevice2; BluetoothDevice expectedMainBluetoothDevice = preferredDevice.getDevice(); mCachedDevice3.setGroupId(GROUP1); - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); when(mBroadcast.isEnabled(null)).thenReturn(false); assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) @@ -457,16 +470,20 @@ public class CsipDeviceManagerTest { assertThat(mCachedDevice1.getDevice()).isEqualTo(expectedMainBluetoothDevice); verify(mAssistant, never()).addSource(any(BluetoothDevice.class), any(BluetoothLeBroadcastMetadata.class), anyBoolean()); + verify(mDevice2, never()).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, + TEMP_BOND_METADATA.getBytes()); + verify(mDevice3, never()).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, + TEMP_BOND_METADATA.getBytes()); } @Test - public void addMemberDevicesIntoMainDevice_preferredDeviceIsMemberAndTwoMain_syncSource() { + @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) + public void addMemberDevicesIntoMainDevice_preferredDeviceIsMemberAndTwoMain_syncState() { // Condition: The preferredDevice is member and there are two main device in top list // Expected Result: return true and there is the preferredDevice in top list CachedBluetoothDevice preferredDevice = mCachedDevice2; BluetoothDevice expectedMainBluetoothDevice = preferredDevice.getDevice(); mCachedDevice3.setGroupId(GROUP1); - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); when(mBroadcast.isEnabled(null)).thenReturn(true); BluetoothLeBroadcastMetadata metadata = Mockito.mock(BluetoothLeBroadcastMetadata.class); when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(metadata); @@ -474,6 +491,8 @@ public class CsipDeviceManagerTest { BluetoothLeBroadcastReceiveState.class); when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L)); when(mAssistant.getAllSources(mDevice1)).thenReturn(ImmutableList.of(state)); + when(mDevice1.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) + .thenReturn(TEMP_BOND_METADATA.getBytes()); assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) .isTrue(); @@ -488,6 +507,10 @@ public class CsipDeviceManagerTest { assertThat(mCachedDevice1.getDevice()).isEqualTo(expectedMainBluetoothDevice); verify(mAssistant).addSource(mDevice2, metadata, /* isGroupOp= */ false); verify(mAssistant).addSource(mDevice3, metadata, /* isGroupOp= */ false); + verify(mDevice2).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, + TEMP_BOND_METADATA.getBytes()); + verify(mDevice3).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, + TEMP_BOND_METADATA.getBytes()); } @Test diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java index 93ebc84374b2..7b6604b3f1c6 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java @@ -942,7 +942,7 @@ public class InfoMediaManagerTest { assertThat(mInfoMediaManager.getCurrentConnectedDevice()).isEqualTo(device); } - @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING) @Test public void composePreferenceRouteListing_useSystemOrderingIsFalse() { RouteListingPreference routeListingPreference = @@ -955,7 +955,7 @@ public class InfoMediaManagerTest { assertThat(routeOrder.get(1).getRouteId()).isEqualTo(TEST_ID_4); } - @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING) @Test public void composePreferenceRouteListing_useSystemOrderingIsTrue() { RouteListingPreference routeListingPreference = @@ -968,7 +968,7 @@ public class InfoMediaManagerTest { assertThat(routeOrder.get(1).getRouteId()).isEqualTo(TEST_ID_3); } - @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING) @Test public void arrangeRouteListByPreference_useSystemOrderingIsFalse() { RouteListingPreference routeListingPreference = @@ -986,7 +986,7 @@ public class InfoMediaManagerTest { assertThat(routeOrder.get(3).getId()).isEqualTo(TEST_ID_1); } - @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING) @Test public void arrangeRouteListByPreference_useSystemOrderingIsTrue() { RouteListingPreference routeListingPreference = diff --git a/packages/SettingsLib/tests/unit/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExtTest.kt b/packages/SettingsLib/tests/unit/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExtTest.kt index 1ad20dc02042..5f6eb5e49573 100644 --- a/packages/SettingsLib/tests/unit/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExtTest.kt +++ b/packages/SettingsLib/tests/unit/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExtTest.kt @@ -233,7 +233,7 @@ class BluetoothLeBroadcastMetadataExtTest { const val QR_CODE_STRING = "BLUETOOTH:UUID:184F;BN:VGVzdA==;AT:1;AD:00A1A1A1A1A1;BI:1E240;BC:VGVzdENvZGU=;" + - "MD:BgNwVGVzdA==;AS:1;PI:A0;NS:1;BS:3;NB:2;SM:BQNUZXN0BARlbmc=;;" + "PM:BgNwVGVzdA==;AS:1;PI:A0;NS:1;BS:3;NB:2;SM:BQNUZXN0BARlbmc=;;" const val QR_CODE_STRING_NON_ENCRYPTED = "BLUETOOTH:UUID:184F;BN:SG9ja2V5;AT:0;AD:AABBCC001122;BI:DE51E9;SQ:1;AS:1;PI:FFFF;" + "NS:1;BS:1;NB:1;;" diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index 246aa7158cab..85617bad1a91 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -809,7 +809,9 @@ public class SettingsBackupTest { Settings.Secure.DND_CONFIGS_MIGRATED, Settings.Secure.NAVIGATION_MODE_RESTORE, Settings.Secure.V_TO_U_RESTORE_ALLOWLIST, - Settings.Secure.V_TO_U_RESTORE_DENYLIST); + Settings.Secure.V_TO_U_RESTORE_DENYLIST, + Settings.Secure.REDACT_OTP_NOTIFICATION_WHILE_CONNECTED_TO_WIFI, + Settings.Secure.REDACT_OTP_NOTIFICATION_IMMEDIATELY); @Test public void systemSettingsBackedUpOrDenied() { diff --git a/packages/Shell/src/com/android/shell/BugreportPrefs.java b/packages/Shell/src/com/android/shell/BugreportPrefs.java index 93690d48cd04..b0fd925daec3 100644 --- a/packages/Shell/src/com/android/shell/BugreportPrefs.java +++ b/packages/Shell/src/com/android/shell/BugreportPrefs.java @@ -23,25 +23,24 @@ import android.content.SharedPreferences; * Preferences related to bug reports. */ final class BugreportPrefs { - static final String PREFS_BUGREPORT = "bugreports"; - - private static final String KEY_WARNING_STATE = "warning-state"; - - static final int STATE_UNKNOWN = 0; - // Shows the warning dialog. - static final int STATE_SHOW = 1; - // Skips the warning dialog. - static final int STATE_HIDE = 2; static int getWarningState(Context context, int def) { - final SharedPreferences prefs = context.getSharedPreferences( - PREFS_BUGREPORT, Context.MODE_PRIVATE); - return prefs.getInt(KEY_WARNING_STATE, def); + String prefsBugreport = context.getResources().getString( + com.android.internal.R.string.prefs_bugreport); + String keyWarningState = context.getResources().getString( + com.android.internal.R.string.key_warning_state); + final SharedPreferences prefs = context.getSharedPreferences(prefsBugreport, + Context.MODE_PRIVATE); + return prefs.getInt(keyWarningState, def); } static void setWarningState(Context context, int value) { - final SharedPreferences prefs = context.getSharedPreferences( - PREFS_BUGREPORT, Context.MODE_PRIVATE); - prefs.edit().putInt(KEY_WARNING_STATE, value).apply(); + String prefsBugreport = context.getResources().getString( + com.android.internal.R.string.prefs_bugreport); + String keyWarningState = context.getResources().getString( + com.android.internal.R.string.key_warning_state); + final SharedPreferences prefs = context.getSharedPreferences(prefsBugreport, + Context.MODE_PRIVATE); + prefs.edit().putInt(keyWarningState, value).apply(); } } diff --git a/packages/Shell/src/com/android/shell/BugreportProgressService.java b/packages/Shell/src/com/android/shell/BugreportProgressService.java index 61f49db07abc..fb0678fedb56 100644 --- a/packages/Shell/src/com/android/shell/BugreportProgressService.java +++ b/packages/Shell/src/com/android/shell/BugreportProgressService.java @@ -21,8 +21,6 @@ import static android.content.pm.PackageManager.FEATURE_LEANBACK; import static android.content.pm.PackageManager.FEATURE_TELEVISION; import static android.os.Process.THREAD_PRIORITY_BACKGROUND; -import static com.android.shell.BugreportPrefs.STATE_HIDE; -import static com.android.shell.BugreportPrefs.STATE_UNKNOWN; import static com.android.shell.BugreportPrefs.getWarningState; import static com.android.shell.flags.Flags.handleBugreportsForWear; @@ -1347,7 +1345,11 @@ public class BugreportProgressService extends Service { } private boolean hasUserDecidedNotToGetWarningMessage() { - return getWarningState(mContext, STATE_UNKNOWN) == STATE_HIDE; + int bugreportStateUnknown = mContext.getResources().getInteger( + com.android.internal.R.integer.bugreport_state_unknown); + int bugreportStateHide = mContext.getResources().getInteger( + com.android.internal.R.integer.bugreport_state_hide); + return getWarningState(mContext, bugreportStateUnknown) == bugreportStateHide; } private void maybeShowWarningMessageAndCloseNotification(int id) { diff --git a/packages/Shell/src/com/android/shell/BugreportWarningActivity.java b/packages/Shell/src/com/android/shell/BugreportWarningActivity.java index a44e23603f52..0e835f91aca6 100644 --- a/packages/Shell/src/com/android/shell/BugreportWarningActivity.java +++ b/packages/Shell/src/com/android/shell/BugreportWarningActivity.java @@ -16,9 +16,6 @@ package com.android.shell; -import static com.android.shell.BugreportPrefs.STATE_HIDE; -import static com.android.shell.BugreportPrefs.STATE_SHOW; -import static com.android.shell.BugreportPrefs.STATE_UNKNOWN; import static com.android.shell.BugreportPrefs.getWarningState; import static com.android.shell.BugreportPrefs.setWarningState; import static com.android.shell.BugreportProgressService.sendShareIntent; @@ -69,12 +66,19 @@ public class BugreportWarningActivity extends AlertActivity mConfirmRepeat = (CheckBox) ap.mView.findViewById(android.R.id.checkbox); - final int state = getWarningState(this, STATE_UNKNOWN); + int bugreportStateUnknown = getResources().getInteger( + com.android.internal.R.integer.bugreport_state_unknown); + int bugreportStateHide = getResources().getInteger( + com.android.internal.R.integer.bugreport_state_hide); + int bugreportStateShow = getResources().getInteger( + com.android.internal.R.integer.bugreport_state_show); + + final int state = getWarningState(this, bugreportStateUnknown); final boolean checked; if (Build.IS_USER) { - checked = state == STATE_HIDE; // Only checks if specifically set to. + checked = state == bugreportStateHide; // Only checks if specifically set to. } else { - checked = state != STATE_SHOW; // Checks by default. + checked = state != bugreportStateShow; // Checks by default. } mConfirmRepeat.setChecked(checked); @@ -83,9 +87,14 @@ public class BugreportWarningActivity extends AlertActivity @Override public void onClick(DialogInterface dialog, int which) { + int bugreportStateHide = getResources().getInteger( + com.android.internal.R.integer.bugreport_state_hide); + int bugreportStateShow = getResources().getInteger( + com.android.internal.R.integer.bugreport_state_show); if (which == AlertDialog.BUTTON_POSITIVE) { // Remember confirm state, and launch target - setWarningState(this, mConfirmRepeat.isChecked() ? STATE_HIDE : STATE_SHOW); + setWarningState(this, mConfirmRepeat.isChecked() ? bugreportStateHide + : bugreportStateShow); if (mSendIntent != null) { sendShareIntent(this, mSendIntent); } diff --git a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java index 7bda2ea790b0..2d6abe6cdc93 100644 --- a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java +++ b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java @@ -19,10 +19,6 @@ package com.android.shell; import static android.test.MoreAsserts.assertContainsRegex; import static com.android.shell.ActionSendMultipleConsumerActivity.UI_NAME; -import static com.android.shell.BugreportPrefs.PREFS_BUGREPORT; -import static com.android.shell.BugreportPrefs.STATE_HIDE; -import static com.android.shell.BugreportPrefs.STATE_SHOW; -import static com.android.shell.BugreportPrefs.STATE_UNKNOWN; import static com.android.shell.BugreportPrefs.getWarningState; import static com.android.shell.BugreportPrefs.setWarningState; import static com.android.shell.BugreportProgressService.INTENT_BUGREPORT_REQUESTED; @@ -201,8 +197,9 @@ public class BugreportReceiverTest { return null; }).when(mMockIDumpstate).startBugreport(anyInt(), any(), any(), any(), anyInt(), anyInt(), any(), anyBoolean(), anyBoolean()); - - setWarningState(mContext, STATE_HIDE); + int bugreportStateHide = mContext.getResources().getInteger( + com.android.internal.R.integer.bugreport_state_hide); + setWarningState(mContext, bugreportStateHide); mUiBot.turnScreenOn(); } @@ -469,22 +466,31 @@ public class BugreportReceiverTest { @Test public void testBugreportFinished_withWarningUnknownState() throws Exception { - bugreportFinishedWithWarningTest(STATE_UNKNOWN); + int bugreportStateUnknown = mContext.getResources().getInteger( + com.android.internal.R.integer.bugreport_state_unknown); + bugreportFinishedWithWarningTest(bugreportStateUnknown); } @Test public void testBugreportFinished_withWarningShowAgain() throws Exception { - bugreportFinishedWithWarningTest(STATE_SHOW); + int bugreportStateShow = mContext.getResources().getInteger( + com.android.internal.R.integer.bugreport_state_show); + bugreportFinishedWithWarningTest(bugreportStateShow); } private void bugreportFinishedWithWarningTest(Integer propertyState) throws Exception { + int bugreportStateUnknown = mContext.getResources().getInteger( + com.android.internal.R.integer.bugreport_state_unknown); + int bugreportStateHide = mContext.getResources().getInteger( + com.android.internal.R.integer.bugreport_state_hide); if (propertyState == null) { // Clear properties - mContext.getSharedPreferences(PREFS_BUGREPORT, Context.MODE_PRIVATE) - .edit().clear().commit(); + mContext.getSharedPreferences( + mContext.getResources().getString(com.android.internal.R.string.prefs_bugreport) + , Context.MODE_PRIVATE).edit().clear().commit(); // Confidence check... - assertEquals("Did not reset properties", STATE_UNKNOWN, - getWarningState(mContext, STATE_UNKNOWN)); + assertEquals("Did not reset properties", bugreportStateUnknown, + getWarningState(mContext, bugreportStateUnknown)); } else { setWarningState(mContext, propertyState); } @@ -501,7 +507,8 @@ public class BugreportReceiverTest { // TODO: get ok and dontShowAgain from the dialog reference above UiObject dontShowAgain = mUiBot.getVisibleObject(mContext.getString(R.string.bugreport_confirm_dont_repeat)); - final boolean firstTime = propertyState == null || propertyState == STATE_UNKNOWN; + final boolean firstTime = + propertyState == null || propertyState == bugreportStateUnknown; if (firstTime) { if (Build.IS_USER) { assertFalse("Checkbox should NOT be checked by default on user builds", @@ -524,8 +531,8 @@ public class BugreportReceiverTest { assertActionSendMultiple(extras); // Make sure it's hidden now. - int newState = getWarningState(mContext, STATE_UNKNOWN); - assertEquals("Didn't change state", STATE_HIDE, newState); + int newState = getWarningState(mContext, bugreportStateUnknown); + assertEquals("Didn't change state", bugreportStateHide, newState); } @Test diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 744388f47d0e..19806e7cdf64 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -207,6 +207,8 @@ filegroup { "tests/src/**/systemui/statusbar/notification/row/NotificationConversationInfoTest.java", "tests/src/**/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt", "tests/src/**/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapperTest.kt", + "tests/src/**/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java", + "tests/src/**/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierDisabledTest.java", "tests/src/**/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java", "tests/src/**/systemui/statusbar/phone/CentralSurfacesImplTest.java", "tests/src/**/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java", @@ -538,6 +540,7 @@ android_library { kotlincflags: [ "-Xjvm-default=all", "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", + "-P plugin:androidx.compose.compiler.plugins.kotlin:sourceInformation=true", ], plugins: [ @@ -552,6 +555,11 @@ android_library { }, } +platform_compat_config { + name: "SystemUI-core-compat-config", + src: ":SystemUI-core", +} + filegroup { name: "AAA-src", srcs: ["tests/src/com/android/AAAPlusPlusVerifySysuiRequiredTestPropertiesTest.java"], @@ -754,6 +762,7 @@ android_library { "kosmos", "testables", "androidx.test.rules", + "platform-compat-test-rules", ], libs: [ "android.test.runner.stubs.system", @@ -888,6 +897,7 @@ android_robolectric_test { static_libs: [ "RoboTestLibraries", "androidx.compose.runtime_runtime", + "platform-compat-test-rules", ], libs: [ "android.test.runner.impl", diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 5b989cb6abc4..910f71276376 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -1869,20 +1869,6 @@ flag { bug: "385194612" } -flag{ - name: "gsf_bouncer" - namespace: "systemui" - description: "Applies GSF font styles to Bouncer surfaces." - bug: "379364381" -} - -flag { - name: "gsf_quick_settings" - namespace: "systemui" - description: "Applies GSF font styles to Quick Settings surfaces." - bug: "379364381" -} - flag { name: "spatial_model_launcher_pushback" namespace: "systemui" @@ -1960,6 +1946,16 @@ flag { } flag { + name: "unfold_latency_tracking_fix" + namespace: "systemui" + description: "New implementation to track unfold latency that excludes broken cases" + bug: "390649568" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "ui_rich_ongoing_force_expanded" namespace: "systemui" description: "Force promoted notifications to always be expanded" diff --git a/packages/SystemUI/compose/core/Android.bp b/packages/SystemUI/compose/core/Android.bp index c63c2b48638c..9c6bb2c8f778 100644 --- a/packages/SystemUI/compose/core/Android.bp +++ b/packages/SystemUI/compose/core/Android.bp @@ -42,6 +42,9 @@ android_library { "//frameworks/libs/systemui:tracinglib-platform", ], - kotlincflags: ["-Xjvm-default=all"], + kotlincflags: [ + "-Xjvm-default=all", + "-P plugin:androidx.compose.compiler.plugins.kotlin:sourceInformation=true", + ], use_resource_processor: true, } 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 4a4607b6e8fc..0b17a3f71bda 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 @@ -200,19 +200,15 @@ fun CommunalContainer( scene( CommunalScenes.Blank, userActions = - if (viewModel.v2FlagEnabled()) emptyMap() - else mapOf(Swipe.Start(fromSource = Edge.End) to CommunalScenes.Communal), + if (viewModel.swipeToHubEnabled()) + mapOf(Swipe.Start(fromSource = Edge.End) to CommunalScenes.Communal) + else emptyMap(), ) { // This scene shows nothing only allowing for transitions to the communal scene. Box(modifier = Modifier.fillMaxSize()) } - scene( - CommunalScenes.Communal, - userActions = - if (viewModel.v2FlagEnabled()) emptyMap() - else mapOf(Swipe.End to CommunalScenes.Blank), - ) { + scene(CommunalScenes.Communal, userActions = mapOf(Swipe.End to CommunalScenes.Blank)) { CommunalScene( backgroundType = backgroundType, colors = colors, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 3c0480d150e0..418a7a52a97e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -1705,15 +1705,38 @@ private fun Umo( contentScope: ContentScope?, modifier: Modifier = Modifier, ) { - if (SceneContainerFlag.isEnabled && contentScope != null) { - contentScope.MediaCarousel( - modifier = modifier.fillMaxSize(), - isVisible = true, - mediaHost = viewModel.mediaHost, - carouselController = viewModel.mediaCarouselController, - ) - } else { - UmoLegacy(viewModel, modifier) + val showNextActionLabel = stringResource(R.string.accessibility_action_label_umo_show_next) + val showPreviousActionLabel = + stringResource(R.string.accessibility_action_label_umo_show_previous) + + Box( + modifier = + modifier.thenIf(!viewModel.isEditMode) { + Modifier.semantics { + customActions = + listOf( + CustomAccessibilityAction(showNextActionLabel) { + viewModel.onShowNextMedia() + true + }, + CustomAccessibilityAction(showPreviousActionLabel) { + viewModel.onShowPreviousMedia() + true + }, + ) + } + } + ) { + if (SceneContainerFlag.isEnabled && contentScope != null) { + contentScope.MediaCarousel( + modifier = modifier.fillMaxSize(), + isVisible = true, + mediaHost = viewModel.mediaHost, + carouselController = viewModel.mediaCarouselController, + ) + } else { + UmoLegacy(viewModel, modifier) + } } } @@ -1724,7 +1747,7 @@ private fun UmoLegacy(viewModel: BaseCommunalViewModel, modifier: Modifier = Mod modifier .clip( shape = - RoundedCornerShape(dimensionResource(system_app_widget_background_radius)) + RoundedCornerShape(dimensionResource(R.dimen.notification_corner_radius)) ) .background(MaterialTheme.colorScheme.primary) .pointerInput(Unit) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt index 62aa31b49870..73a24257580c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt @@ -50,7 +50,6 @@ import androidx.compose.ui.unit.times import androidx.window.layout.WindowMetricsCalculator import com.android.systemui.communal.util.WindowSizeUtils.COMPACT_HEIGHT import com.android.systemui.communal.util.WindowSizeUtils.COMPACT_WIDTH -import com.android.systemui.communal.util.WindowSizeUtils.MEDIUM_WIDTH /** * Renders a responsive [LazyHorizontalGrid] with dynamic columns and rows. Each cell will maintain @@ -267,9 +266,8 @@ fun calculateWindowSize(): DpSize { } private fun calculateNumCellsWidth(width: Dp) = - // See https://developer.android.com/develop/ui/views/layout/use-window-size-classes when { - width >= MEDIUM_WIDTH -> 3 + width >= 900.dp -> 3 width >= COMPACT_WIDTH -> 2 else -> 1 } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt index ba25719f1d60..0abed39dce6b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt @@ -26,18 +26,16 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalDensity import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters +import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.plugins.clocks.ClockController import kotlin.math.min import kotlin.math.roundToInt /** Produces a [BurnInState] that can be used to query the `LockscreenBurnInViewModel` flows. */ @Composable -fun rememberBurnIn( - clockInteractor: KeyguardClockInteractor, -): BurnInState { - val clock by clockInteractor.currentClock.collectAsStateWithLifecycle() +fun rememberBurnIn(clockViewModel: KeyguardClockViewModel): BurnInState { + val clock by clockViewModel.currentClock.collectAsStateWithLifecycle() val (smartspaceTop, onSmartspaceTopChanged) = remember { mutableStateOf<Float?>(null) } val (smallClockTop, onSmallClockTopChanged) = remember { mutableStateOf<Float?>(null) } @@ -62,18 +60,12 @@ fun rememberBurnIn( } @Composable -private fun rememberBurnInParameters( - clock: ClockController?, - topmostTop: Int, -): BurnInParameters { +private fun rememberBurnInParameters(clock: ClockController?, topmostTop: Int): BurnInParameters { val density = LocalDensity.current val topInset = WindowInsets.systemBars.union(WindowInsets.displayCutout).getTop(density) return remember(clock, topInset, topmostTop) { - BurnInParameters( - topInset = topInset, - minViewY = topmostTop, - ) + BurnInParameters(topInset = topInset, minViewY = topmostTop) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt index abf7fdc05f2e..f51049a10569 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt @@ -38,11 +38,11 @@ import com.android.compose.animation.scene.ContentScope import com.android.compose.modifiers.thenIf import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn import com.android.systemui.keyguard.ui.composable.modifier.burnInAware import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters +import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.notifications.ui.composable.ConstrainedNotificationStack @@ -89,7 +89,7 @@ constructor( private val nicAodIconViewStore: AlwaysOnDisplayNotificationIconViewStore, private val aodPromotedNotificationViewModelFactory: AODPromotedNotificationViewModel.Factory, private val systemBarUtilsState: SystemBarUtilsState, - private val clockInteractor: KeyguardClockInteractor, + private val keyguardClockViewModel: KeyguardClockViewModel, ) { init { @@ -118,7 +118,7 @@ constructor( val isVisible by keyguardRootViewModel.isAodPromotedNotifVisible.collectAsStateWithLifecycle() - val burnIn = rememberBurnIn(clockInteractor) + val burnIn = rememberBurnIn(keyguardClockViewModel) AnimatedVisibility( visible = isVisible, @@ -141,7 +141,7 @@ constructor( isVisible.stopAnimating() } } - val burnIn = rememberBurnIn(clockInteractor) + val burnIn = rememberBurnIn(keyguardClockViewModel) AnimatedVisibility( visibleState = transitionState, enter = fadeIn(), diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt index 410499a3c23f..6293fc26f96a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt @@ -37,7 +37,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.rememberMutableSceneTransitionLayoutState import com.android.compose.modifiers.thenIf -import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.largeClockScene import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.smallClockScene import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.splitShadeLargeClockScene @@ -56,7 +55,7 @@ constructor( private val mediaCarouselSection: MediaCarouselSection, private val clockSection: DefaultClockSection, private val weatherClockSection: WeatherClockSection, - private val clockInteractor: KeyguardClockInteractor, + private val keyguardClockViewModel: KeyguardClockViewModel, ) { @Composable fun ContentScope.DefaultClockLayout( @@ -138,7 +137,7 @@ constructor( smartSpacePaddingTop: (Resources) -> Int, modifier: Modifier = Modifier, ) { - val burnIn = rememberBurnIn(clockInteractor) + val burnIn = rememberBurnIn(keyguardClockViewModel) Column(modifier = modifier) { with(clockSection) { @@ -163,7 +162,7 @@ constructor( smartSpacePaddingTop: (Resources) -> Int, shouldOffSetClockToOneHalf: Boolean = false, ) { - val burnIn = rememberBurnIn(clockInteractor) + val burnIn = rememberBurnIn(keyguardClockViewModel) val isLargeClockVisible by clockViewModel.isLargeClockVisible.collectAsStateWithLifecycle() LaunchedEffect(isLargeClockVisible) { @@ -204,7 +203,7 @@ constructor( smartSpacePaddingTop: (Resources) -> Int, modifier: Modifier = Modifier, ) { - val burnIn = rememberBurnIn(clockInteractor) + val burnIn = rememberBurnIn(keyguardClockViewModel) val isLargeClockVisible by clockViewModel.isLargeClockVisible.collectAsStateWithLifecycle() val currentClockState = clockViewModel.currentClock.collectAsStateWithLifecycle() diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt index d7d4e1714aa6..09b8d178cc8e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt @@ -175,7 +175,7 @@ fun ContentScope.SnoozeableHeadsUpNotificationSpace( viewModel: NotificationsPlaceholderViewModel, ) { - val isHeadsUp by viewModel.isHeadsUpOrAnimatingAway.collectAsStateWithLifecycle(false) + val isSnoozable by viewModel.isHeadsUpOrAnimatingAway.collectAsStateWithLifecycle(false) var scrollOffset by remember { mutableFloatStateOf(0f) } val headsUpInset = with(LocalDensity.current) { headsUpTopInset().toPx() } @@ -192,7 +192,7 @@ fun ContentScope.SnoozeableHeadsUpNotificationSpace( ) } - val nestedScrollConnection = + val snoozeScrollConnection = object : NestedScrollConnection { override suspend fun onPreFling(available: Velocity): Velocity { if ( @@ -206,7 +206,7 @@ fun ContentScope.SnoozeableHeadsUpNotificationSpace( } } - LaunchedEffect(isHeadsUp) { scrollOffset = 0f } + LaunchedEffect(isSnoozable) { scrollOffset = 0f } LaunchedEffect(scrollableState.isScrollInProgress) { if (!scrollableState.isScrollInProgress && scrollOffset <= minScrollOffset) { @@ -230,10 +230,8 @@ fun ContentScope.SnoozeableHeadsUpNotificationSpace( ), ) } - .thenIf(isHeadsUp) { - Modifier.nestedScroll(nestedScrollConnection) - .scrollable(orientation = Orientation.Vertical, state = scrollableState) - }, + .thenIf(isSnoozable) { Modifier.nestedScroll(snoozeScrollConnection) } + .scrollable(orientation = Orientation.Vertical, state = scrollableState), ) } 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 7c50d6f8af12..64f3cb13662a 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 @@ -30,9 +30,9 @@ 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.dagger.SysUISingleton -import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn import com.android.systemui.keyguard.ui.composable.section.DefaultClockSection +import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayActionsViewModel import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayContentViewModel @@ -57,7 +57,7 @@ constructor( private val shadeSession: SaveableSession, private val stackScrollView: Lazy<NotificationScrollView>, private val clockSection: DefaultClockSection, - private val clockInteractor: KeyguardClockInteractor, + private val keyguardClockViewModel: KeyguardClockViewModel, ) : Overlay { override val key = Overlays.NotificationsShade @@ -86,7 +86,7 @@ constructor( OverlayShade( panelElement = NotificationsShade.Elements.Panel, - panelAlignment = Alignment.TopStart, + alignmentOnWideScreens = Alignment.TopStart, modifier = modifier, onScrimClicked = viewModel::onScrimClicked, header = { @@ -105,7 +105,7 @@ constructor( Box { Column { if (viewModel.showClock) { - val burnIn = rememberBurnIn(clockInteractor) + val burnIn = rememberBurnIn(keyguardClockViewModel) with(clockSection) { SmallClock( 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 cc58b8e13744..afdb3cbba60e 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 @@ -128,7 +128,7 @@ constructor( ) OverlayShade( panelElement = QuickSettingsShade.Elements.Panel, - panelAlignment = Alignment.TopEnd, + alignmentOnWideScreens = Alignment.TopEnd, onScrimClicked = contentViewModel::onScrimClicked, header = { OverlayShadeHeader( 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 da4e5824eb3e..aa0d474ba41c 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 @@ -46,6 +46,8 @@ import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.observableTransitionState import com.android.compose.animation.scene.rememberMutableSceneTransitionLayoutState import com.android.compose.gesture.effect.rememberOffsetOverscrollEffectFactory +import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn +import com.android.systemui.keyguard.ui.composable.modifier.burnInAware import com.android.systemui.lifecycle.rememberActivated import com.android.systemui.qs.ui.adapter.QSSceneAdapter import com.android.systemui.qs.ui.composable.QuickSettingsTheme @@ -202,7 +204,7 @@ fun SceneContainer( SceneTransitionLayout( state = state, modifier = modifier.fillMaxSize(), - swipeSourceDetector = viewModel.edgeDetector, + swipeSourceDetector = viewModel.swipeSourceDetector, ) { sceneByKey.forEach { (sceneKey, scene) -> scene( @@ -239,7 +241,12 @@ fun SceneContainer( BottomRightCornerRibbon( content = { Text(text = "flexi\uD83E\uDD43", color = Color.White) }, colorSaturation = { viewModel.ribbonColorSaturation }, - modifier = Modifier.align(Alignment.BottomEnd), + modifier = + Modifier.align(Alignment.BottomEnd) + .burnInAware( + viewModel = viewModel.burnIn, + params = rememberBurnIn(viewModel.clock).parameters, + ), ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt index 1423d4acca21..6d906bd4aa66 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt @@ -59,10 +59,7 @@ class SceneTransitionLayoutDataSource( initialValue = emptySet(), ) - override fun changeScene( - toScene: SceneKey, - transitionKey: TransitionKey?, - ) { + override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) { state.setTargetScene( targetScene = toScene, transitionKey = transitionKey, @@ -71,9 +68,7 @@ class SceneTransitionLayoutDataSource( } override fun snapToScene(toScene: SceneKey) { - state.snapToScene( - scene = toScene, - ) + state.snapToScene(scene = toScene) } override fun showOverlay(overlay: OverlayKey, transitionKey: TransitionKey?) { @@ -100,4 +95,18 @@ class SceneTransitionLayoutDataSource( transitionKey = transitionKey, ) } + + override fun instantlyShowOverlay(overlay: OverlayKey) { + state.snapToScene( + scene = state.transitionState.currentScene, + currentOverlays = state.currentOverlays + overlay, + ) + } + + override fun instantlyHideOverlay(overlay: OverlayKey) { + state.snapToScene( + scene = state.transitionState.currentScene, + currentOverlays = state.currentOverlays - overlay, + ) + } } 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 5dcec5b8836d..cdb1e2e53b09 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 @@ -59,7 +59,7 @@ import com.android.systemui.res.R @Composable fun ContentScope.OverlayShade( panelElement: ElementKey, - panelAlignment: Alignment, + alignmentOnWideScreens: Alignment, onScrimClicked: () -> Unit, modifier: Modifier = Modifier, header: @Composable () -> Unit, @@ -71,7 +71,7 @@ fun ContentScope.OverlayShade( Box( modifier = Modifier.fillMaxSize().panelContainerPadding(isFullWidth), - contentAlignment = panelAlignment, + contentAlignment = if (isFullWidth) Alignment.TopCenter else alignmentOnWideScreens, ) { Panel( modifier = diff --git a/packages/SystemUI/compose/scene/Android.bp b/packages/SystemUI/compose/scene/Android.bp index 090e9ccedda0..42dd85a3d0a7 100644 --- a/packages/SystemUI/compose/scene/Android.bp +++ b/packages/SystemUI/compose/scene/Android.bp @@ -45,6 +45,9 @@ android_library { "mechanics", ], - kotlincflags: ["-Xjvm-default=all"], + kotlincflags: [ + "-Xjvm-default=all", + "-P plugin:androidx.compose.compiler.plugins.kotlin:sourceInformation=true", + ], use_resource_processor: true, } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/testing/ElementStateAccess.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/testing/ElementStateAccess.kt index cade9bff5abb..ad2ddfe2b2a4 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/testing/ElementStateAccess.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/testing/ElementStateAccess.kt @@ -16,9 +16,12 @@ package com.android.compose.animation.scene.testing +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.semantics.SemanticsNode +import androidx.compose.ui.unit.IntSize import com.android.compose.animation.scene.Element import com.android.compose.animation.scene.Element.Companion.AlphaUnspecified +import com.android.compose.animation.scene.Element.Companion.SizeUnspecified import com.android.compose.animation.scene.ElementModifier import com.android.compose.animation.scene.Scale @@ -28,6 +31,12 @@ val SemanticsNode.lastAlphaForTesting: Float? val SemanticsNode.lastScaleForTesting: Scale? get() = elementState.lastScale.takeIf { it != Scale.Unspecified } +val SemanticsNode.lastOffsetForTesting: Offset? + get() = elementState.lastOffset.takeIf { it != Offset.Unspecified } + +val SemanticsNode.lastSizeForTesting: IntSize? + get() = elementState.lastSize.takeIf { it != SizeUnspecified } + private val SemanticsNode.elementState: Element.State get() { val elementModifier = diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/DataPointTypes.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/DataPointTypes.kt new file mode 100644 index 000000000000..7be7fa17eeea --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/DataPointTypes.kt @@ -0,0 +1,84 @@ +/* + * 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.compose.animation.scene + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.isFinite +import androidx.compose.ui.geometry.isUnspecified +import org.json.JSONObject +import platform.test.motion.golden.DataPointType +import platform.test.motion.golden.UnknownTypeException + +fun Scale.asDataPoint() = DataPointTypes.scale.makeDataPoint(this) + +object DataPointTypes { + val scale: DataPointType<Scale> = + DataPointType( + "scale", + jsonToValue = { + when (it) { + "unspecified" -> Scale.Unspecified + "default" -> Scale.Default + "zero" -> Scale.Zero + is JSONObject -> { + val pivot = it.get("pivot") + Scale( + scaleX = it.getDouble("x").toFloat(), + scaleY = it.getDouble("y").toFloat(), + pivot = + when (pivot) { + "unspecified" -> Offset.Unspecified + "infinite" -> Offset.Infinite + is JSONObject -> + Offset( + pivot.getDouble("x").toFloat(), + pivot.getDouble("y").toFloat(), + ) + else -> throw UnknownTypeException() + }, + ) + } + else -> throw UnknownTypeException() + } + }, + valueToJson = { + when (it) { + Scale.Unspecified -> "unspecified" + Scale.Default -> "default" + Scale.Zero -> "zero" + else -> { + JSONObject().apply { + put("x", it.scaleX) + put("y", it.scaleY) + put( + "pivot", + when { + it.pivot.isUnspecified -> "unspecified" + !it.pivot.isFinite -> "infinite" + else -> + JSONObject().apply { + put("x", it.pivot.x) + put("y", it.pivot.y) + } + }, + ) + } + } + } + }, + ) +} diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/FeatureCaptures.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/FeatureCaptures.kt new file mode 100644 index 000000000000..8658bbf1da56 --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/FeatureCaptures.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.compose.animation.scene + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.semantics.SemanticsNode +import androidx.compose.ui.unit.IntSize +import com.android.compose.animation.scene.DataPointTypes.scale +import com.android.compose.animation.scene.testing.lastAlphaForTesting +import com.android.compose.animation.scene.testing.lastOffsetForTesting +import com.android.compose.animation.scene.testing.lastScaleForTesting +import com.android.compose.animation.scene.testing.lastSizeForTesting +import platform.test.motion.compose.DataPointTypes.intSize +import platform.test.motion.compose.DataPointTypes.offset +import platform.test.motion.golden.DataPoint +import platform.test.motion.golden.DataPointTypes +import platform.test.motion.golden.FeatureCapture + +/** + * [FeatureCapture] implementations to record animated state of [SceneTransitionLayout] [Element]. + */ +object FeatureCaptures { + + val elementAlpha = + FeatureCapture<SemanticsNode, Float>("alpha") { + DataPoint.of(it.lastAlphaForTesting, DataPointTypes.float) + } + + val elementScale = + FeatureCapture<SemanticsNode, Scale>("scale") { + DataPoint.of(it.lastScaleForTesting, scale) + } + + val elementOffset = + FeatureCapture<SemanticsNode, Offset>("offset") { + DataPoint.of(it.lastOffsetForTesting, offset) + } + + val elementSize = + FeatureCapture<SemanticsNode, IntSize>("size") { + DataPoint.of(it.lastSizeForTesting, intSize) + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt index 2e5b5b56c982..aad1276d76e5 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt @@ -113,8 +113,8 @@ class DefaultClockProvider( companion object { // 750ms @ 120hz -> 90 frames of animation - // In practice, 45 looks good enough - const val NUM_CLOCK_FONT_ANIMATION_STEPS = 45 + // In practice, 30 looks good enough and limits our memory usage + const val NUM_CLOCK_FONT_ANIMATION_STEPS = 30 val FLEX_TYPEFACE by lazy { // TODO(b/364680873): Move constant to config_clockFontFamily when shipping diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt index fe665e658feb..24b9e847d621 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt @@ -84,6 +84,7 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.GlobalSettings import com.android.systemui.util.time.FakeSystemClock +import com.android.systemui.window.domain.interactor.windowRootViewBlurInteractor import com.google.common.truth.Truth import junit.framework.Assert import kotlinx.coroutines.flow.MutableStateFlow @@ -280,9 +281,9 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { kosmos.keyguardDismissTransitionInteractor, { primaryBouncerInteractor }, executor, - ) { - deviceEntryInteractor - } + { deviceEntryInteractor }, + { kosmos.windowRootViewBlurInteractor }, + ) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java index bd33e52689c2..f53f964cd3d9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java @@ -64,12 +64,12 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; -import java.util.List; -import java.util.Optional; - import platform.test.runner.parameterized.ParameterizedAndroidJunit4; import platform.test.runner.parameterized.Parameters; +import java.util.List; +import java.util.Optional; + @SmallTest @RunWith(ParameterizedAndroidJunit4.class) @EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX) @@ -171,6 +171,7 @@ public class BouncerFullscreenSwipeTouchHandlerTest extends SysuiTestCase { mActivityStarter, mKeyguardInteractor, mSceneInteractor, + mKosmos.getShadeRepository(), Optional.of(() -> mWindowRootView)); when(mScrimManager.getCurrentController()).thenReturn(mScrimController); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java index 494e0b4deef4..dd43d817cccc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java @@ -74,12 +74,12 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; -import java.util.List; -import java.util.Optional; - import platform.test.runner.parameterized.ParameterizedAndroidJunit4; import platform.test.runner.parameterized.Parameters; +import java.util.List; +import java.util.Optional; + @SmallTest @RunWith(ParameterizedAndroidJunit4.class) @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX) @@ -187,6 +187,7 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { mActivityStarter, mKeyguardInteractor, mSceneInteractor, + mKosmos.getShadeRepository(), Optional.of(() -> mWindowRootView) ); @@ -627,6 +628,22 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { onRemovedCallbackCaptor.getValue().onRemoved(); } + @Test + public void testTouchSessionStart_notifiesShadeOfUserInteraction() { + mTouchHandler.onSessionStart(mTouchSession); + + mKosmos.getTestScope().getTestScheduler().runCurrent(); + assertThat(mKosmos.getShadeRepository().getLegacyShadeTracking().getValue()).isTrue(); + + ArgumentCaptor<TouchHandler.TouchSession.Callback> onRemovedCallbackCaptor = + ArgumentCaptor.forClass(TouchHandler.TouchSession.Callback.class); + verify(mTouchSession).registerCallback(onRemovedCallbackCaptor.capture()); + onRemovedCallbackCaptor.getValue().onRemoved(); + + mKosmos.getTestScope().getTestScheduler().runCurrent(); + assertThat(mKosmos.getShadeRepository().getLegacyShadeTracking().getValue()).isFalse(); + } + private void swipeToPosition(float percent, float velocityY) { Mockito.clearInvocations(mTouchSession); mTouchHandler.onSessionStart(mTouchSession); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt index d8a9719d2058..dda460a6198f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt @@ -30,22 +30,17 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.isFinite -import androidx.compose.ui.geometry.isUnspecified -import androidx.compose.ui.semantics.SemanticsNode import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import com.android.compose.animation.scene.ContentScope +import com.android.compose.animation.scene.FeatureCaptures.elementAlpha +import com.android.compose.animation.scene.FeatureCaptures.elementScale import com.android.compose.animation.scene.ObservableTransitionState -import com.android.compose.animation.scene.Scale import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.isElement -import com.android.compose.animation.scene.testing.lastAlphaForTesting -import com.android.compose.animation.scene.testing.lastScaleForTesting import com.android.compose.theme.PlatformTheme import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.domain.interactor.bouncerInteractor @@ -71,12 +66,12 @@ import com.android.systemui.scene.ui.composable.Scene import com.android.systemui.scene.ui.composable.SceneContainer import com.android.systemui.scene.ui.view.sceneJankMonitorFactory import com.android.systemui.testKosmos +import kotlin.test.Ignore import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf -import org.json.JSONObject import org.junit.Before import org.junit.Rule import org.junit.Test @@ -89,14 +84,8 @@ import platform.test.motion.compose.MotionControl import platform.test.motion.compose.feature import platform.test.motion.compose.recordMotion import platform.test.motion.compose.runTest -import platform.test.motion.golden.DataPoint -import platform.test.motion.golden.DataPointType -import platform.test.motion.golden.DataPointTypes -import platform.test.motion.golden.FeatureCapture -import platform.test.motion.golden.UnknownTypeException import platform.test.screenshot.DeviceEmulationSpec import platform.test.screenshot.Displays.Phone -import kotlin.test.Ignore /** MotionTest for the Bouncer Predictive Back animation */ @LargeTest @@ -280,72 +269,4 @@ class BouncerPredictiveBackTest : SysuiTestCase() { override suspend fun onActivated() = awaitCancellation() } - - companion object { - private val elementAlpha = - FeatureCapture<SemanticsNode, Float>("alpha") { - DataPoint.of(it.lastAlphaForTesting, DataPointTypes.float) - } - - private val elementScale = - FeatureCapture<SemanticsNode, Scale>("scale") { - DataPoint.of(it.lastScaleForTesting, scale) - } - - private val scale: DataPointType<Scale> = - DataPointType( - "scale", - jsonToValue = { - when (it) { - "unspecified" -> Scale.Unspecified - "default" -> Scale.Default - "zero" -> Scale.Zero - is JSONObject -> { - val pivot = it.get("pivot") - Scale( - scaleX = it.getDouble("x").toFloat(), - scaleY = it.getDouble("y").toFloat(), - pivot = - when (pivot) { - "unspecified" -> Offset.Unspecified - "infinite" -> Offset.Infinite - is JSONObject -> - Offset( - pivot.getDouble("x").toFloat(), - pivot.getDouble("y").toFloat(), - ) - else -> throw UnknownTypeException() - }, - ) - } - else -> throw UnknownTypeException() - } - }, - valueToJson = { - when (it) { - Scale.Unspecified -> "unspecified" - Scale.Default -> "default" - Scale.Zero -> "zero" - else -> { - JSONObject().apply { - put("x", it.scaleX) - put("y", it.scaleY) - put( - "pivot", - when { - it.pivot.isUnspecified -> "unspecified" - !it.pivot.isFinite -> "infinite" - else -> - JSONObject().apply { - put("x", it.pivot.x) - put("y", it.pivot.y) - } - }, - ) - } - } - } - }, - ) - } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/DeviceInactiveConditionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/DeviceInactiveConditionTest.kt new file mode 100644 index 000000000000..0c97750ba281 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/DeviceInactiveConditionTest.kt @@ -0,0 +1,100 @@ +/* + * 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.communal + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.keyguard.keyguardUpdateMonitor +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP +import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.keyguard.shared.model.DozeStateModel +import com.android.systemui.keyguard.shared.model.DozeTransitionModel +import com.android.systemui.keyguard.wakefulnessLifecycle +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.statusbar.policy.keyguardStateController +import com.android.systemui.testKosmos +import com.android.systemui.util.kotlin.JavaAdapter +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +class DeviceInactiveConditionTest : SysuiTestCase() { + private val kosmos = + testKosmos().useUnconfinedTestDispatcher().also { + whenever(it.wakefulnessLifecycle.wakefulness) doReturn WAKEFULNESS_AWAKE + } + + private val Kosmos.underTest by + Kosmos.Fixture { + DeviceInactiveCondition( + applicationCoroutineScope, + keyguardStateController, + wakefulnessLifecycle, + keyguardUpdateMonitor, + keyguardInteractor, + JavaAdapter(applicationCoroutineScope), + ) + } + + @Test + fun asleep_conditionTrue() = + kosmos.runTest { + // Condition is false to start. + underTest.start() + assertThat(underTest.isConditionMet).isFalse() + + // Condition is true when device goes to sleep. + sleep() + assertThat(underTest.isConditionMet).isTrue() + } + + @Test + fun dozingAndAsleep_conditionFalse() = + kosmos.runTest { + // Condition is true when device is asleep. + underTest.start() + sleep() + assertThat(underTest.isConditionMet).isTrue() + + // Condition turns false after doze starts. + fakeKeyguardRepository.setDozeTransitionModel( + DozeTransitionModel(from = DozeStateModel.UNINITIALIZED, to = DozeStateModel.DOZE) + ) + assertThat(underTest.isConditionMet).isFalse() + } + + fun Kosmos.sleep() { + whenever(wakefulnessLifecycle.wakefulness) doReturn WAKEFULNESS_ASLEEP + argumentCaptor<WakefulnessLifecycle.Observer>().apply { + verify(wakefulnessLifecycle).addObserver(capture()) + firstValue.onStartedGoingToSleep() + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt index 3eb08004ae61..f063655b9f86 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt @@ -18,7 +18,7 @@ package com.android.systemui.communal.data.db import android.content.ComponentName import android.os.UserHandle -import android.os.UserManager +import android.os.userManager import androidx.sqlite.db.SupportSQLiteDatabase import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -27,10 +27,13 @@ import com.android.systemui.communal.data.db.DefaultWidgetPopulation.SkipReason. import com.android.systemui.communal.shared.model.SpanValue import com.android.systemui.communal.widgets.CommunalWidgetHost import com.android.systemui.kosmos.applicationCoroutineScope -import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.log.logcatLogBuffer import com.android.systemui.testKosmos -import kotlinx.coroutines.test.runCurrent +import com.android.systemui.user.data.repository.FakeUserRepository.Companion.MAIN_USER_ID +import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.user.domain.interactor.userLockedInteractor import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -38,6 +41,7 @@ import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never @@ -46,8 +50,7 @@ import org.mockito.kotlin.verify @SmallTest @RunWith(AndroidJUnit4::class) class DefaultWidgetPopulationTest : SysuiTestCase() { - private val kosmos = testKosmos() - private val testScope = kosmos.testScope + private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val communalWidgetHost = mock<CommunalWidgetHost> { @@ -57,11 +60,6 @@ class DefaultWidgetPopulationTest : SysuiTestCase() { private val communalWidgetDao = mock<CommunalWidgetDao>() private val database = mock<SupportSQLiteDatabase>() private val mainUser = UserHandle(0) - private val userManager = - mock<UserManager> { - on { mainUser }.thenReturn(mainUser) - on { getUserSerialNumber(0) }.thenReturn(0) - } private val defaultWidgets = arrayOf( @@ -74,6 +72,7 @@ class DefaultWidgetPopulationTest : SysuiTestCase() { @Before fun setUp() { + kosmos.fakeUserRepository.setUserUnlocked(MAIN_USER_ID, true) underTest = DefaultWidgetPopulation( bgScope = kosmos.applicationCoroutineScope, @@ -81,32 +80,45 @@ class DefaultWidgetPopulationTest : SysuiTestCase() { communalWidgetDaoProvider = { communalWidgetDao }, defaultWidgets = defaultWidgets, logBuffer = logcatLogBuffer("DefaultWidgetPopulationTest"), - userManager = userManager, + userManager = kosmos.userManager, + userLockedInteractor = kosmos.userLockedInteractor, ) } @Test + fun testNoInteractionUntilMainUserUnlocked() = + kosmos.runTest { + kosmos.fakeUserRepository.setUserUnlocked(MAIN_USER_ID, false) + // Database created + underTest.onCreate(database) + verify(communalWidgetHost, never()) + .allocateIdAndBindWidget(provider = any(), user = any()) + kosmos.fakeUserRepository.setUserUnlocked(MAIN_USER_ID, true) + verify(communalWidgetHost, atLeastOnce()) + .allocateIdAndBindWidget(provider = any(), user = any()) + } + + @Test fun testPopulateDefaultWidgetsWhenDatabaseCreated() = - testScope.runTest { + kosmos.runTest { // Database created underTest.onCreate(database) - runCurrent() // Verify default widgets bound verify(communalWidgetHost) .allocateIdAndBindWidget( provider = eq(ComponentName.unflattenFromString(defaultWidgets[0])!!), - user = eq(mainUser), + user = eq(UserHandle(MAIN_USER_ID)), ) verify(communalWidgetHost) .allocateIdAndBindWidget( provider = eq(ComponentName.unflattenFromString(defaultWidgets[1])!!), - user = eq(mainUser), + user = eq(UserHandle(MAIN_USER_ID)), ) verify(communalWidgetHost) .allocateIdAndBindWidget( provider = eq(ComponentName.unflattenFromString(defaultWidgets[2])!!), - user = eq(mainUser), + user = eq(UserHandle(MAIN_USER_ID)), ) // Verify default widgets added in database @@ -138,13 +150,12 @@ class DefaultWidgetPopulationTest : SysuiTestCase() { @Test fun testSkipDefaultWidgetsPopulation() = - testScope.runTest { + kosmos.runTest { // Skip default widgets population underTest.skipDefaultWidgetsPopulation(RESTORED_FROM_BACKUP) // Database created underTest.onCreate(database) - runCurrent() // Verify no widget bounded or added to the database verify(communalWidgetHost, never()).allocateIdAndBindWidget(any(), any()) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index c3cc3e66f81f..8424746f3db5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -75,6 +75,7 @@ import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.settings.fakeUserTracker import com.android.systemui.statusbar.phone.fakeManagedProfileController import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor @@ -163,12 +164,12 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test - fun isCommunalAvailable_storageUnlockedAndMainUser_true() = + fun isCommunalAvailable_mainUserUnlockedAndMainUser_true() = kosmos.runTest { val isAvailable by collectLastValue(underTest.isCommunalAvailable) assertThat(isAvailable).isFalse() - fakeKeyguardRepository.setIsEncryptedOrLockdown(false) + fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true) fakeUserRepository.setSelectedUserInfo(mainUser) fakeKeyguardRepository.setKeyguardShowing(true) @@ -176,12 +177,12 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test - fun isCommunalAvailable_storageLockedAndMainUser_false() = + fun isCommunalAvailable_mainUserLockedAndMainUser_false() = kosmos.runTest { val isAvailable by collectLastValue(underTest.isCommunalAvailable) assertThat(isAvailable).isFalse() - fakeKeyguardRepository.setIsEncryptedOrLockdown(true) + fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, false) fakeUserRepository.setSelectedUserInfo(mainUser) fakeKeyguardRepository.setKeyguardShowing(true) @@ -189,12 +190,12 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test - fun isCommunalAvailable_storageUnlockedAndSecondaryUser_false() = + fun isCommunalAvailable_mainUserUnlockedAndSecondaryUser_false() = kosmos.runTest { val isAvailable by collectLastValue(underTest.isCommunalAvailable) assertThat(isAvailable).isFalse() - fakeKeyguardRepository.setIsEncryptedOrLockdown(false) + fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true) fakeUserRepository.setSelectedUserInfo(secondaryUser) fakeKeyguardRepository.setKeyguardShowing(true) @@ -207,7 +208,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { val isAvailable by collectLastValue(underTest.isCommunalAvailable) assertThat(isAvailable).isFalse() - fakeKeyguardRepository.setIsEncryptedOrLockdown(false) + fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true) fakeUserRepository.setSelectedUserInfo(mainUser) fakeKeyguardRepository.setKeyguardShowing(true) @@ -222,7 +223,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { val isAvailable by collectLastValue(underTest.isCommunalAvailable) assertThat(isAvailable).isFalse() - fakeKeyguardRepository.setIsEncryptedOrLockdown(false) + fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, false) fakeUserRepository.setSelectedUserInfo(mainUser) fakeKeyguardRepository.setKeyguardShowing(true) @@ -1282,7 +1283,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun showCommunalWhileCharging() = kosmos.runTest { - fakeKeyguardRepository.setIsEncryptedOrLockdown(false) + fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true) fakeUserRepository.setSelectedUserInfo(mainUser) fakeKeyguardRepository.setKeyguardShowing(true) fakeSettings.putIntForUser( @@ -1302,7 +1303,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun showCommunalWhilePosturedAndCharging() = kosmos.runTest { - fakeKeyguardRepository.setIsEncryptedOrLockdown(false) + fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true) fakeUserRepository.setSelectedUserInfo(mainUser) fakeKeyguardRepository.setKeyguardShowing(true) fakeSettings.putIntForUser( @@ -1323,7 +1324,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun showCommunalWhileDocked() = kosmos.runTest { - fakeKeyguardRepository.setIsEncryptedOrLockdown(false) + fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true) fakeUserRepository.setSelectedUserInfo(mainUser) fakeKeyguardRepository.setKeyguardShowing(true) fakeSettings.putIntForUser(Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, 1, mainUser.id) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelTest.kt index c158baf5a80c..619995478ecc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelTest.kt @@ -35,7 +35,6 @@ import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.se import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Overlays -import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade import com.android.systemui.shade.domain.interactor.enableDualShade @@ -74,7 +73,7 @@ class CommunalUserActionsViewModelTest : SysuiTestCase() { setUpState(isShadeTouchable = true, isDeviceUnlocked = false) assertThat(actions).isNotEmpty() - assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home)) + assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(Scenes.Lockscreen)) assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer)) assertThat(actions?.get(Swipe.Down)).isEqualTo(UserActionResult(Scenes.Shade)) @@ -83,7 +82,7 @@ class CommunalUserActionsViewModelTest : SysuiTestCase() { setUpState(isShadeTouchable = true, isDeviceUnlocked = true) assertThat(actions).isNotEmpty() - assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home)) + assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(Scenes.Lockscreen)) assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone)) assertThat(actions?.get(Swipe.Down)).isEqualTo(UserActionResult(Scenes.Shade)) } @@ -96,7 +95,7 @@ class CommunalUserActionsViewModelTest : SysuiTestCase() { setUpState(isShadeTouchable = true, isDeviceUnlocked = false) assertThat(actions).isNotEmpty() - assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home)) + assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(Scenes.Lockscreen)) assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer)) assertThat(actions?.get(Swipe.Down)) .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade)) @@ -106,7 +105,7 @@ class CommunalUserActionsViewModelTest : SysuiTestCase() { setUpState(isShadeTouchable = true, isDeviceUnlocked = true) assertThat(actions).isNotEmpty() - assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home)) + assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(Scenes.Lockscreen)) assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone)) assertThat(actions?.get(Swipe.Down)) .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade)) @@ -120,7 +119,7 @@ class CommunalUserActionsViewModelTest : SysuiTestCase() { setUpState(isShadeTouchable = true, isDeviceUnlocked = false) assertThat(actions).isNotEmpty() - assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home)) + assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(Scenes.Lockscreen)) assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer)) assertThat(actions?.get(Swipe.Down)) .isEqualTo(UserActionResult.ShowOverlay(Overlays.NotificationsShade)) @@ -130,7 +129,7 @@ class CommunalUserActionsViewModelTest : SysuiTestCase() { setUpState(isShadeTouchable = true, isDeviceUnlocked = true) assertThat(actions).isNotEmpty() - assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home)) + assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(Scenes.Lockscreen)) assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone)) assertThat(actions?.get(Swipe.Down)) .isEqualTo(UserActionResult.ShowOverlay(Overlays.NotificationsShade)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt index 18cc8bf5f0d3..522650bcde3c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt @@ -23,24 +23,19 @@ import android.content.pm.PackageManager import android.content.pm.UserInfo import android.provider.Settings import android.view.accessibility.AccessibilityEvent -import android.view.accessibility.AccessibilityManager import android.view.accessibility.accessibilityManager import android.widget.RemoteViews import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.internal.logging.UiEventLogger +import com.android.internal.logging.uiEventLogger +import com.android.internal.logging.uiEventLoggerFake import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.model.CommunalSmartspaceTimer -import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository -import com.android.systemui.communal.data.repository.FakeCommunalSmartspaceRepository -import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository -import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository import com.android.systemui.communal.data.repository.fakeCommunalSmartspaceRepository import com.android.systemui.communal.data.repository.fakeCommunalTutorialRepository import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository import com.android.systemui.communal.domain.interactor.CommunalInteractor -import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.domain.interactor.communalSceneInteractor import com.android.systemui.communal.domain.interactor.communalSettingsInteractor @@ -49,12 +44,15 @@ import com.android.systemui.communal.shared.log.CommunalMetricsLogger import com.android.systemui.communal.shared.log.CommunalUiEvent import com.android.systemui.communal.shared.model.EditModeState import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel -import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.log.logcatLogBuffer import com.android.systemui.media.controls.ui.controller.mediaCarouselController import com.android.systemui.media.controls.ui.view.MediaHost @@ -62,73 +60,45 @@ import com.android.systemui.settings.fakeUserTracker import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.fakeUserRepository import com.google.common.truth.Truth.assertThat +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceTimeBy -import kotlinx.coroutines.test.runTest import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.never import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.eq import org.mockito.kotlin.mock -import org.mockito.kotlin.spy import org.mockito.kotlin.whenever +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class CommunalEditModeViewModelTest : SysuiTestCase() { - @Mock private lateinit var mediaHost: MediaHost - @Mock private lateinit var uiEventLogger: UiEventLogger - @Mock private lateinit var packageManager: PackageManager - @Mock private lateinit var metricsLogger: CommunalMetricsLogger - - private val kosmos = testKosmos() - private val testScope = kosmos.testScope - - private lateinit var tutorialRepository: FakeCommunalTutorialRepository - private lateinit var widgetRepository: FakeCommunalWidgetRepository - private lateinit var smartspaceRepository: FakeCommunalSmartspaceRepository - private lateinit var mediaRepository: FakeCommunalMediaRepository - private lateinit var communalSceneInteractor: CommunalSceneInteractor - private lateinit var communalInteractor: CommunalInteractor - private lateinit var accessibilityManager: AccessibilityManager + private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val testableResources = context.orCreateTestableResources - private lateinit var underTest: CommunalEditModeViewModel + private val Kosmos.packageManager by Kosmos.Fixture { mock<PackageManager>() } - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - - tutorialRepository = kosmos.fakeCommunalTutorialRepository - widgetRepository = kosmos.fakeCommunalWidgetRepository - smartspaceRepository = kosmos.fakeCommunalSmartspaceRepository - mediaRepository = kosmos.fakeCommunalMediaRepository - communalSceneInteractor = kosmos.communalSceneInteractor - communalInteractor = spy(kosmos.communalInteractor) - kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO)) - kosmos.fakeUserTracker.set(userInfos = listOf(MAIN_USER_INFO), selectedUserIndex = 0) - kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true) - accessibilityManager = kosmos.accessibilityManager + private val Kosmos.metricsLogger by Kosmos.Fixture { mock<CommunalMetricsLogger>() } - underTest = + private val Kosmos.underTest by + Kosmos.Fixture { CommunalEditModeViewModel( communalSceneInteractor, communalInteractor, - kosmos.communalSettingsInteractor, - kosmos.keyguardTransitionInteractor, - mediaHost, + communalSettingsInteractor, + keyguardTransitionInteractor, + mock<MediaHost>(), uiEventLogger, logcatLogBuffer("CommunalEditModeViewModelTest"), - kosmos.testDispatcher, + testDispatcher, metricsLogger, context, accessibilityManager, @@ -136,19 +106,28 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { WIDGET_PICKER_PACKAGE_NAME, kosmos.mediaCarouselController, ) + } + + @Before + fun setUp() { + kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO)) + kosmos.fakeUserTracker.set(userInfos = listOf(MAIN_USER_INFO), selectedUserIndex = 0) + kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true) } @Test fun communalContent_onlyWidgetsAndCtaTileAreShownInEditMode() = - testScope.runTest { - tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) + kosmos.runTest { + fakeCommunalTutorialRepository.setTutorialSettingState( + Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED + ) // Widgets available. - widgetRepository.addWidget(appWidgetId = 0, rank = 30) - widgetRepository.addWidget(appWidgetId = 1, rank = 20) + fakeCommunalWidgetRepository.addWidget(appWidgetId = 0, rank = 30) + fakeCommunalWidgetRepository.addWidget(appWidgetId = 1, rank = 20) // Smartspace available. - smartspaceRepository.setTimers( + fakeCommunalSmartspaceRepository.setTimers( listOf( CommunalSmartspaceTimer( smartspaceTargetId = "target", @@ -159,7 +138,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { ) // Media playing. - mediaRepository.mediaActive() + fakeCommunalMediaRepository.mediaActive() val communalContent by collectLastValue(underTest.communalContent) @@ -173,7 +152,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { @Test fun selectedKey_onReorderWidgets_isSet() = - testScope.runTest { + kosmos.runTest { val selectedKey by collectLastValue(underTest.selectedKey) underTest.setSelectedKey(null) @@ -186,7 +165,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { @Test fun isCommunalContentVisible_isTrue_whenEditModeShowing() = - testScope.runTest { + kosmos.runTest { val isCommunalContentVisible by collectLastValue(underTest.isCommunalContentVisible) communalSceneInteractor.setEditModeState(EditModeState.SHOWING) assertThat(isCommunalContentVisible).isEqualTo(true) @@ -194,7 +173,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { @Test fun isCommunalContentVisible_isFalse_whenEditModeNotShowing() = - testScope.runTest { + kosmos.runTest { val isCommunalContentVisible by collectLastValue(underTest.isCommunalContentVisible) communalSceneInteractor.setEditModeState(null) assertThat(isCommunalContentVisible).isEqualTo(false) @@ -202,12 +181,14 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { @Test fun deleteWidget() = - testScope.runTest { - tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) + kosmos.runTest { + fakeCommunalTutorialRepository.setTutorialSettingState( + Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED + ) // Widgets available. - widgetRepository.addWidget(appWidgetId = 0, rank = 30) - widgetRepository.addWidget(appWidgetId = 1, rank = 20) + fakeCommunalWidgetRepository.addWidget(appWidgetId = 0, rank = 30) + fakeCommunalWidgetRepository.addWidget(appWidgetId = 1, rank = 20) val communalContent by collectLastValue(underTest.communalContent) @@ -233,26 +214,38 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { } @Test - fun reorderWidget_uiEventLogging_start() { - underTest.onReorderWidgetStart(CommunalContentModel.KEY.widget(123)) - verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_START) - } + fun reorderWidget_uiEventLogging_start() = + kosmos.runTest { + underTest.onReorderWidgetStart(CommunalContentModel.KEY.widget(123)) + + assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1) + assertThat(uiEventLoggerFake.logs[0].eventId) + .isEqualTo(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_START.id) + } @Test - fun reorderWidget_uiEventLogging_end() { - underTest.onReorderWidgetEnd() - verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_FINISH) - } + fun reorderWidget_uiEventLogging_end() = + kosmos.runTest { + underTest.onReorderWidgetEnd() + + assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1) + assertThat(uiEventLoggerFake.logs[0].eventId) + .isEqualTo(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_FINISH.id) + } @Test - fun reorderWidget_uiEventLogging_cancel() { - underTest.onReorderWidgetCancel() - verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL) - } + fun reorderWidget_uiEventLogging_cancel() = + kosmos.runTest { + underTest.onReorderWidgetCancel() + + assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1) + assertThat(uiEventLoggerFake.logs[0].eventId) + .isEqualTo(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL.id) + } @Test fun onOpenWidgetPicker_launchesWidgetPickerActivity() { - testScope.runTest { + kosmos.runTest { var activityStarted = false val success = underTest.onOpenWidgetPicker(testableResources.resources) { _ -> @@ -266,7 +259,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { @Test fun onOpenWidgetPicker_activityLaunchThrowsException_failure() { - testScope.runTest { + kosmos.runTest { val success = underTest.onOpenWidgetPicker(testableResources.resources) { _ -> run { throw ActivityNotFoundException() } @@ -278,7 +271,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { @Test fun showDisclaimer_trueAfterEditModeShowing() = - testScope.runTest { + kosmos.runTest { val showDisclaimer by collectLastValue(underTest.showDisclaimer) assertThat(showDisclaimer).isFalse() @@ -288,9 +281,9 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { @Test fun showDisclaimer_falseWhenDismissed() = - testScope.runTest { + kosmos.runTest { underTest.setEditModeState(EditModeState.SHOWING) - kosmos.fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO) + fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO) val showDisclaimer by collectLastValue(underTest.showDisclaimer) @@ -301,63 +294,67 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { @Test fun showDisclaimer_trueWhenTimeout() = - testScope.runTest { + kosmos.runTest { underTest.setEditModeState(EditModeState.SHOWING) - kosmos.fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO) + fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO) val showDisclaimer by collectLastValue(underTest.showDisclaimer) assertThat(showDisclaimer).isTrue() underTest.onDisclaimerDismissed() assertThat(showDisclaimer).isFalse() - advanceTimeBy(CommunalInteractor.DISCLAIMER_RESET_MILLIS) + testScope.advanceTimeBy(CommunalInteractor.DISCLAIMER_RESET_MILLIS + 1.milliseconds) assertThat(showDisclaimer).isTrue() } @Test - fun scrollPosition_persistedOnEditCleanup() { - val index = 2 - val offset = 30 - underTest.onScrollPositionUpdated(index, offset) - underTest.cleanupEditModeState() - - verify(communalInteractor).setScrollPosition(eq(index), eq(offset)) - } + fun scrollPosition_persistedOnEditCleanup() = + kosmos.runTest { + val index = 2 + val offset = 30 + underTest.onScrollPositionUpdated(index, offset) + underTest.cleanupEditModeState() + + assertThat(communalInteractor.firstVisibleItemIndex).isEqualTo(index) + assertThat(communalInteractor.firstVisibleItemOffset).isEqualTo(offset) + } @Test - fun onNewWidgetAdded_accessibilityDisabled_doNothing() { - whenever(accessibilityManager.isEnabled).thenReturn(false) + fun onNewWidgetAdded_accessibilityDisabled_doNothing() = + kosmos.runTest { + whenever(accessibilityManager.isEnabled).thenReturn(false) - val provider = - mock<AppWidgetProviderInfo> { - on { loadLabel(packageManager) }.thenReturn("Test Clock") - } - underTest.onNewWidgetAdded(provider) + val provider = + mock<AppWidgetProviderInfo> { + on { loadLabel(packageManager) }.thenReturn("Test Clock") + } + underTest.onNewWidgetAdded(provider) - verify(accessibilityManager, never()).sendAccessibilityEvent(any()) - } + verify(accessibilityManager, never()).sendAccessibilityEvent(any()) + } @Test - fun onNewWidgetAdded_accessibilityEnabled_sendAccessibilityAnnouncement() { - whenever(accessibilityManager.isEnabled).thenReturn(true) + fun onNewWidgetAdded_accessibilityEnabled_sendAccessibilityAnnouncement() = + kosmos.runTest { + whenever(accessibilityManager.isEnabled).thenReturn(true) - val provider = - mock<AppWidgetProviderInfo> { - on { loadLabel(packageManager) }.thenReturn("Test Clock") - } - underTest.onNewWidgetAdded(provider) + val provider = + mock<AppWidgetProviderInfo> { + on { loadLabel(packageManager) }.thenReturn("Test Clock") + } + underTest.onNewWidgetAdded(provider) - val captor = argumentCaptor<AccessibilityEvent>() - verify(accessibilityManager).sendAccessibilityEvent(captor.capture()) + val captor = argumentCaptor<AccessibilityEvent>() + verify(accessibilityManager).sendAccessibilityEvent(captor.capture()) - val event = captor.firstValue - assertThat(event.eventType).isEqualTo(AccessibilityEvent.TYPE_ANNOUNCEMENT) - assertThat(event.contentDescription).isEqualTo("Test Clock widget added to lock screen") - } + val event = captor.firstValue + assertThat(event.eventType).isEqualTo(AccessibilityEvent.TYPE_ANNOUNCEMENT) + assertThat(event.contentDescription).isEqualTo("Test Clock widget added to lock screen") + } @Test fun onResizeWidget_logsMetrics() = - testScope.runTest { + kosmos.runTest { val appWidgetId = 123 val spanY = 2 val widgetIdToRankMap = mapOf(appWidgetId to 1) @@ -372,7 +369,6 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { rank = rank, ) - verify(communalInteractor).resizeWidget(appWidgetId, spanY, widgetIdToRankMap) verify(metricsLogger) .logResizeWidget( componentName = componentName.flattenToString(), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt index dbdd7fb2773a..799054a92bee 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt @@ -78,6 +78,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager import com.android.systemui.media.controls.ui.controller.mediaCarouselController +import com.android.systemui.media.controls.ui.view.MediaCarouselScrollHandler import com.android.systemui.media.controls.ui.view.MediaHost import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest import com.android.systemui.power.domain.interactor.powerInteractor @@ -120,6 +121,7 @@ import platform.test.runner.parameterized.Parameters @RunWith(ParameterizedAndroidJunit4::class) class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { @Mock private lateinit var mediaHost: MediaHost + @Mock private lateinit var mediaCarouselScrollHandler: MediaCarouselScrollHandler @Mock private lateinit var metricsLogger: CommunalMetricsLogger private val kosmos = testKosmos() @@ -161,6 +163,8 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { kosmos.fakeUserTracker.set(userInfos = listOf(MAIN_USER_INFO), selectedUserIndex = 0) whenever(mediaHost.visible).thenReturn(true) + whenever(kosmos.mediaCarouselController.mediaCarouselScrollHandler) + .thenReturn(mediaCarouselScrollHandler) kosmos.powerInteractor.setAwakeForTest() @@ -187,6 +191,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { metricsLogger, kosmos.mediaCarouselController, kosmos.blurConfig, + false, ) } @@ -203,7 +208,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { // Keyguard showing, storage unlocked, main user, and tutorial not started. keyguardRepository.setKeyguardShowing(true) keyguardRepository.setKeyguardOccluded(false) - keyguardRepository.setIsEncryptedOrLockdown(false) + userRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true) setIsMainUser(true) tutorialRepository.setTutorialSettingState( Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED @@ -903,6 +908,20 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test + fun onShowPreviousMedia_scrollHandler_isCalled() = + testScope.runTest { + underTest.onShowPreviousMedia() + verify(mediaCarouselScrollHandler).scrollByStep(-1) + } + + @Test + fun onShowNextMedia_scrollHandler_isCalled() = + testScope.runTest { + underTest.onShowNextMedia() + verify(mediaCarouselScrollHandler).scrollByStep(1) + } + + @Test @EnableFlags(FLAG_BOUNCER_UI_REVAMP) fun uiIsBlurred_whenPrimaryBouncerIsShowing() = testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt index 95681941a1c1..c15f797aad5d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt @@ -37,7 +37,9 @@ import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.settings.fakeUserTracker import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.FakeUserRepository.Companion.MAIN_USER_ID import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.user.domain.interactor.userLockedInteractor import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat @@ -91,6 +93,7 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { kosmos.testDispatcher, { widgetManager }, helper, + kosmos.userLockedInteractor, ) } @@ -269,6 +272,7 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { // Binding to the service does not require keyguard showing setCommunalAvailable(true, setKeyguardShowing = false) + fakeKeyguardRepository.setIsEncryptedOrLockdown(false) runCurrent() verify(widgetManager).register() @@ -283,7 +287,7 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { setKeyguardShowing: Boolean = true, ) = with(kosmos) { - fakeKeyguardRepository.setIsEncryptedOrLockdown(false) + fakeUserRepository.setUserUnlocked(MAIN_USER_ID, true) fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO) if (setKeyguardShowing) { fakeKeyguardRepository.setKeyguardShowing(true) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt index 1c93b3c66e32..102ce0b51c94 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt @@ -23,7 +23,6 @@ import com.android.compose.animation.scene.UserActionResult import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel -import com.android.systemui.communal.domain.interactor.setCommunalAvailable import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor import com.android.systemui.flags.EnableSceneContainer @@ -62,15 +61,14 @@ class DreamUserActionsViewModelTest : SysuiTestCase() { @Before fun setUp() { - underTest = kosmos.dreamUserActionsViewModel + underTest = kosmos.dreamUserActionsViewModelFactory.create() underTest.activateIn(testScope) } @Test - fun actions_communalNotAvailable_singleShade() = + fun actions_singleShade() = testScope.runTest { kosmos.enableSingleShade() - kosmos.setCommunalAvailable(false) val actions by collectLastValue(underTest.actions) @@ -93,10 +91,9 @@ class DreamUserActionsViewModelTest : SysuiTestCase() { } @Test - fun actions_communalNotAvailable_splitShade() = + fun actions_splitShade() = testScope.runTest { kosmos.enableSplitShade() - kosmos.setCommunalAvailable(false) val actions by collectLastValue(underTest.actions) @@ -121,10 +118,9 @@ class DreamUserActionsViewModelTest : SysuiTestCase() { } @Test - fun actions_communalNotAvailable_dualShade() = + fun actions_dualShade() = testScope.runTest { kosmos.enableDualShade() - kosmos.setCommunalAvailable(false) val actions by collectLastValue(underTest.actions) @@ -148,88 +144,6 @@ class DreamUserActionsViewModelTest : SysuiTestCase() { assertThat(actions?.get(Swipe.End)).isNull() } - @Test - fun actions_communalAvailable_singleShade() = - testScope.runTest { - kosmos.enableSingleShade() - kosmos.setCommunalAvailable(true) - - val actions by collectLastValue(underTest.actions) - - setUpState(isShadeTouchable = true, isDeviceUnlocked = false) - assertThat(actions).isNotEmpty() - assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer)) - assertThat(actions?.get(Swipe.Down)).isEqualTo(UserActionResult(Scenes.Shade)) - assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal)) - assertThat(actions?.get(Swipe.End)).isNull() - - setUpState(isShadeTouchable = false, isDeviceUnlocked = false) - assertThat(actions).isEmpty() - - setUpState(isShadeTouchable = true, isDeviceUnlocked = true) - assertThat(actions).isNotEmpty() - assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone)) - assertThat(actions?.get(Swipe.Down)).isEqualTo(UserActionResult(Scenes.Shade)) - assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal)) - assertThat(actions?.get(Swipe.End)).isNull() - } - - @Test - fun actions_communalAvailable_splitShade() = - testScope.runTest { - kosmos.enableSplitShade() - kosmos.setCommunalAvailable(true) - - val actions by collectLastValue(underTest.actions) - - setUpState(isShadeTouchable = true, isDeviceUnlocked = false) - assertThat(actions).isNotEmpty() - assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer)) - assertThat(actions?.get(Swipe.Down)) - .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade)) - assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal)) - assertThat(actions?.get(Swipe.End)).isNull() - - setUpState(isShadeTouchable = false, isDeviceUnlocked = false) - assertThat(actions).isEmpty() - - setUpState(isShadeTouchable = true, isDeviceUnlocked = true) - assertThat(actions).isNotEmpty() - assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone)) - assertThat(actions?.get(Swipe.Down)) - .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade)) - assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal)) - assertThat(actions?.get(Swipe.End)).isNull() - } - - @Test - fun actions_communalAvailable_dualShade() = - testScope.runTest { - kosmos.enableDualShade() - kosmos.setCommunalAvailable(true) - - val actions by collectLastValue(underTest.actions) - - setUpState(isShadeTouchable = true, isDeviceUnlocked = false) - assertThat(actions).isNotEmpty() - assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer)) - assertThat(actions?.get(Swipe.Down)) - .isEqualTo(UserActionResult.ShowOverlay(Overlays.NotificationsShade)) - assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal)) - assertThat(actions?.get(Swipe.End)).isNull() - - setUpState(isShadeTouchable = false, isDeviceUnlocked = false) - assertThat(actions).isEmpty() - - setUpState(isShadeTouchable = true, isDeviceUnlocked = true) - assertThat(actions).isNotEmpty() - assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone)) - assertThat(actions?.get(Swipe.Down)) - .isEqualTo(UserActionResult.ShowOverlay(Overlays.NotificationsShade)) - assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal)) - assertThat(actions?.get(Swipe.End)).isNull() - } - private fun TestScope.setUpState(isShadeTouchable: Boolean, isDeviceUnlocked: Boolean) { if (isShadeTouchable) { kosmos.powerInteractor.setAwakeForTest() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt index e13f3f12c55a..7e93f5a8c9a8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt @@ -20,19 +20,26 @@ import android.os.PowerManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization +import android.provider.Settings import android.service.dream.dreamManager import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState import com.android.systemui.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.Flags.FLAG_COMMUNAL_SCENE_KTF_REFACTOR +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR import com.android.systemui.Flags.FLAG_SCENE_CONTAINER +import com.android.systemui.Flags.glanceableHubV2 import com.android.systemui.SysuiTestCase +import com.android.systemui.common.data.repository.batteryRepository +import com.android.systemui.common.data.repository.fake import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository import com.android.systemui.communal.data.repository.communalSceneRepository import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository import com.android.systemui.communal.domain.interactor.setCommunalAvailable +import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled 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.keyguard.data.repository.fakeKeyguardTransitionRepositorySpy import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository @@ -56,11 +63,14 @@ import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.se import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth import junit.framework.Assert.assertEquals import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -93,7 +103,10 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT @JvmStatic @Parameters(name = "{0}") fun getParams(): List<FlagsParameterization> { - return FlagsParameterization.allCombinationsOf(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) + return FlagsParameterization.allCombinationsOf( + FLAG_COMMUNAL_SCENE_KTF_REFACTOR, + FLAG_GLANCEABLE_HUB_V2, + ) } } @@ -107,6 +120,7 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT // Transition to DOZING and set the power interactor asleep. kosmos.powerInteractor.setAsleepForTest() + kosmos.setCommunalV2ConfigEnabled(true) runBlocking { kosmos.transitionRepository.sendTransitionSteps( from = KeyguardState.LOCKSCREEN, @@ -160,7 +174,7 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT @Test @EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR) - @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) + @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR, FLAG_GLANCEABLE_HUB_V2) fun testTransitionToLockscreen_onWake_canDream_glanceableHubAvailable() = kosmos.runTest { whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) @@ -179,7 +193,17 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT fun testTransitionToLockscreen_onWake_canDream_ktfRefactor() = kosmos.runTest { setCommunalAvailable(true) - whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) + if (glanceableHubV2()) { + val user = fakeUserRepository.asMainUser() + fakeSettings.putIntForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, + 1, + user.id, + ) + batteryRepository.fake.setDevicePluggedIn(true) + } else { + whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) + } clearInvocations(fakeCommunalSceneRepository) powerInteractor.setAwakeForTest() @@ -240,7 +264,17 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT fun testTransitionToGlanceableHub_onWakeup_ifAvailable() = kosmos.runTest { setCommunalAvailable(true) - whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) + if (glanceableHubV2()) { + val user = fakeUserRepository.asMainUser() + fakeSettings.putIntForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, + 1, + user.id, + ) + batteryRepository.fake.setDevicePluggedIn(true) + } else { + whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) + } // Device turns on. powerInteractor.setAwakeForTest() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt index 8e1068226431..5882cff74eb6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt @@ -19,14 +19,20 @@ package com.android.systemui.keyguard.domain.interactor import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization +import android.provider.Settings import android.service.dream.dreamManager import androidx.test.filters.SmallTest import com.android.systemui.Flags import com.android.systemui.Flags.FLAG_COMMUNAL_SCENE_KTF_REFACTOR +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 +import com.android.systemui.Flags.glanceableHubV2 import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository +import com.android.systemui.common.data.repository.batteryRepository +import com.android.systemui.common.data.repository.fake import com.android.systemui.communal.data.repository.communalSceneRepository import com.android.systemui.communal.domain.interactor.setCommunalAvailable +import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.flags.andSceneContainer import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository @@ -46,6 +52,8 @@ import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.se import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.advanceTimeBy @@ -66,7 +74,10 @@ class FromDreamingTransitionInteractorTest(flags: FlagsParameterization?) : Sysu @JvmStatic @Parameters(name = "{0}") fun getParams(): List<FlagsParameterization> { - return FlagsParameterization.allCombinationsOf(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) + return FlagsParameterization.allCombinationsOf( + FLAG_COMMUNAL_SCENE_KTF_REFACTOR, + FLAG_GLANCEABLE_HUB_V2, + ) .andSceneContainer() } } @@ -101,6 +112,7 @@ class FromDreamingTransitionInteractorTest(flags: FlagsParameterization?) : Sysu ) reset(kosmos.transitionRepository) kosmos.setCommunalAvailable(true) + kosmos.setCommunalV2ConfigEnabled(true) } kosmos.underTest.start() } @@ -202,7 +214,17 @@ class FromDreamingTransitionInteractorTest(flags: FlagsParameterization?) : Sysu reset(transitionRepository) setCommunalAvailable(true) - whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) + if (glanceableHubV2()) { + val user = fakeUserRepository.asMainUser() + fakeSettings.putIntForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, + 1, + user.id, + ) + batteryRepository.fake.setDevicePluggedIn(true) + } else { + whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) + } // Device wakes up. powerInteractor.setAwakeForTest() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt index cf8123764928..47ca4b14a26f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt @@ -29,7 +29,6 @@ import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel -import com.android.systemui.communal.domain.interactor.setCommunalAvailable import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository import com.android.systemui.flags.EnableSceneContainer @@ -42,7 +41,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.TransitionKeys -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.disableDualShade import com.android.systemui.shade.domain.interactor.enableDualShade @@ -65,13 +64,12 @@ import platform.test.runner.parameterized.Parameters class LockscreenUserActionsViewModelTest : SysuiTestCase() { companion object { - private const val parameterCount = 7 + private const val parameterCount = 6 @Parameters( name = "canSwipeToEnter={0}, downWithTwoPointers={1}, downFromEdge={2}," + - " isSingleShade={3}, isCommunalAvailable={4}, isShadeTouchable={5}," + - " isOccluded={6}" + " isSingleShade={3}, isShadeTouchable={4}, isOccluded={6}" ) @JvmStatic fun combinations() = buildList { @@ -82,9 +80,8 @@ class LockscreenUserActionsViewModelTest : SysuiTestCase() { /* downWithTwoPointers= */ combination and 2 != 0, /* downFromEdge= */ combination and 4 != 0, /* isSingleShade= */ combination and 8 != 0, - /* isCommunalAvailable= */ combination and 16 != 0, - /* isShadeTouchable= */ combination and 32 != 0, - /* isOccluded= */ combination and 64 != 0, + /* isShadeTouchable= */ combination and 16 != 0, + /* isOccluded= */ combination and 32 != 0, ) .also { check(it.size == parameterCount) } ) @@ -145,17 +142,6 @@ class LockscreenUserActionsViewModelTest : SysuiTestCase() { else -> Scenes.Bouncer } } - - private fun expectedStartDestination( - isCommunalAvailable: Boolean, - isShadeTouchable: Boolean, - ): SceneKey? { - return when { - !isShadeTouchable -> null - isCommunalAvailable -> Scenes.Communal - else -> null - } - } } private val kosmos = testKosmos() @@ -166,9 +152,8 @@ class LockscreenUserActionsViewModelTest : SysuiTestCase() { @JvmField @Parameter(1) var downWithTwoPointers: Boolean = false @JvmField @Parameter(2) var downFromEdge: Boolean = false @JvmField @Parameter(3) var isNarrowScreen: Boolean = true - @JvmField @Parameter(4) var isCommunalAvailable: Boolean = false - @JvmField @Parameter(5) var isShadeTouchable: Boolean = false - @JvmField @Parameter(6) var isOccluded: Boolean = false + @JvmField @Parameter(4) var isShadeTouchable: Boolean = false + @JvmField @Parameter(5) var isOccluded: Boolean = false private val underTest by lazy { kosmos.lockscreenUserActionsViewModel } @@ -188,7 +173,6 @@ class LockscreenUserActionsViewModelTest : SysuiTestCase() { ) sceneInteractor.changeScene(Scenes.Lockscreen, "reason") kosmos.shadeRepository.setShadeLayoutWide(!isNarrowScreen) - kosmos.setCommunalAvailable(isCommunalAvailable) kosmos.fakePowerRepository.updateWakefulness( rawState = if (isShadeTouchable) { @@ -246,22 +230,6 @@ class LockscreenUserActionsViewModelTest : SysuiTestCase() { isShadeTouchable = isShadeTouchable, ) ) - - val startScene by - collectLastValue( - (userActions?.get(Swipe.Start) as? UserActionResult.ChangeScene) - ?.toScene - ?.let { scene -> kosmos.sceneInteractor.resolveSceneFamily(scene) } - ?: flowOf(null) - ) - - assertThat(startScene) - .isEqualTo( - expectedStartDestination( - isCommunalAvailable = isCommunalAvailable, - isShadeTouchable = isShadeTouchable, - ) - ) } @Test @@ -279,7 +247,6 @@ class LockscreenUserActionsViewModelTest : SysuiTestCase() { ) sceneInteractor.changeScene(Scenes.Lockscreen, "reason") kosmos.enableDualShade(wideLayout = !isNarrowScreen) - kosmos.setCommunalAvailable(isCommunalAvailable) kosmos.fakePowerRepository.updateWakefulness( rawState = if (isShadeTouchable) { @@ -308,20 +275,20 @@ class LockscreenUserActionsViewModelTest : SysuiTestCase() { assertThat(downDestination?.transitionKey).isNull() } - val downFromTopRightDestination = + val downFromEndHalfDestination = userActions?.get( Swipe.Down( - fromSource = SceneContainerEdge.TopRight, + fromSource = SceneContainerArea.EndHalf, pointerCount = if (downWithTwoPointers) 2 else 1, ) ) when { - !isShadeTouchable -> assertThat(downFromTopRightDestination).isNull() - downWithTwoPointers -> assertThat(downFromTopRightDestination).isNull() + !isShadeTouchable -> assertThat(downFromEndHalfDestination).isNull() + downWithTwoPointers -> assertThat(downFromEndHalfDestination).isNull() else -> { - assertThat(downFromTopRightDestination) + assertThat(downFromEndHalfDestination) .isEqualTo(ShowOverlay(Overlays.QuickSettingsShade)) - assertThat(downFromTopRightDestination?.transitionKey).isNull() + assertThat(downFromEndHalfDestination?.transitionKey).isNull() } } @@ -340,21 +307,5 @@ class LockscreenUserActionsViewModelTest : SysuiTestCase() { isShadeTouchable = isShadeTouchable, ) ) - - val startScene by - collectLastValue( - (userActions?.get(Swipe.Start) as? UserActionResult.ChangeScene) - ?.toScene - ?.let { scene -> kosmos.sceneInteractor.resolveSceneFamily(scene) } - ?: flowOf(null) - ) - - assertThat(startScene) - .isEqualTo( - expectedStartDestination( - isCommunalAvailable = isCommunalAvailable, - isShadeTouchable = isShadeTouchable, - ) - ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java index 69485e848a6a..b177e07d09b6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.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. @@ -30,8 +30,9 @@ import static org.mockito.Mockito.when; import android.content.ComponentName; import android.content.pm.PackageManager; -import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.dream.lowlight.LowLightDreamManager; @@ -39,6 +40,8 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.shared.condition.Condition; import com.android.systemui.shared.condition.Monitor; +import com.android.systemui.util.concurrency.FakeExecutor; +import com.android.systemui.util.time.FakeSystemClock; import dagger.Lazy; @@ -53,7 +56,8 @@ import org.mockito.MockitoAnnotations; import java.util.Set; @SmallTest -@RunWith(AndroidTestingRunner.class) +@RunWith(AndroidJUnit4.class) +@TestableLooper.RunWithLooper() public class LowLightMonitorTest extends SysuiTestCase { @Mock @@ -78,6 +82,8 @@ public class LowLightMonitorTest extends SysuiTestCase { @Mock private ComponentName mDreamComponent; + FakeExecutor mBackgroundExecutor = new FakeExecutor(new FakeSystemClock()); + Condition mCondition = mock(Condition.class); Set<Condition> mConditionSet = Set.of(mCondition); @@ -91,12 +97,13 @@ public class LowLightMonitorTest extends SysuiTestCase { when(mLazyConditions.get()).thenReturn(mConditionSet); mLowLightMonitor = new LowLightMonitor(mLowLightDreamManagerLazy, mMonitor, mLazyConditions, mScreenLifecycle, mLogger, mDreamComponent, - mPackageManager); + mPackageManager, mBackgroundExecutor); } @Test public void testSetAmbientLowLightWhenInLowLight() { mLowLightMonitor.onConditionsChanged(true); + mBackgroundExecutor.runAllReady(); // Verify setting low light when condition is true verify(mLowLightDreamManager).setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); } @@ -105,6 +112,7 @@ public class LowLightMonitorTest extends SysuiTestCase { public void testExitAmbientLowLightWhenNotInLowLight() { mLowLightMonitor.onConditionsChanged(true); mLowLightMonitor.onConditionsChanged(false); + mBackgroundExecutor.runAllReady(); // Verify ambient light toggles back to light mode regular verify(mLowLightDreamManager).setAmbientLightMode(AMBIENT_LIGHT_MODE_REGULAR); } @@ -112,6 +120,7 @@ public class LowLightMonitorTest extends SysuiTestCase { @Test public void testStartMonitorLowLightConditionsWhenScreenTurnsOn() { mLowLightMonitor.onScreenTurnedOn(); + mBackgroundExecutor.runAllReady(); // Verify subscribing to low light conditions monitor when screen turns on. verify(mMonitor).addSubscription(any()); @@ -125,6 +134,7 @@ public class LowLightMonitorTest extends SysuiTestCase { // Verify removing subscription when screen turns off. mLowLightMonitor.onScreenTurnedOff(); + mBackgroundExecutor.runAllReady(); verify(mMonitor).removeSubscription(token); } @@ -135,6 +145,7 @@ public class LowLightMonitorTest extends SysuiTestCase { mLowLightMonitor.onScreenTurnedOn(); mLowLightMonitor.onScreenTurnedOn(); + mBackgroundExecutor.runAllReady(); // Verify subscription is only added once. verify(mMonitor, times(1)).addSubscription(any()); } @@ -146,6 +157,7 @@ public class LowLightMonitorTest extends SysuiTestCase { mLowLightMonitor.onScreenTurnedOn(); mLowLightMonitor.onScreenTurnedOn(); + mBackgroundExecutor.runAllReady(); Set<Condition> conditions = captureConditions(); // Verify Monitor is subscribed to the expected conditions assertThat(conditions).isEqualTo(mConditionSet); @@ -154,7 +166,7 @@ public class LowLightMonitorTest extends SysuiTestCase { @Test public void testNotUnsubscribeIfNotSubscribedWhenScreenTurnsOff() { mLowLightMonitor.onScreenTurnedOff(); - + mBackgroundExecutor.runAllReady(); // Verify doesn't remove subscription since there is none. verify(mMonitor, never()).removeSubscription(any()); } @@ -163,6 +175,7 @@ public class LowLightMonitorTest extends SysuiTestCase { public void testSubscribeIfScreenIsOnWhenStarting() { when(mScreenLifecycle.getScreenState()).thenReturn(SCREEN_ON); mLowLightMonitor.start(); + mBackgroundExecutor.runAllReady(); // Verify to add subscription on start if the screen state is on verify(mMonitor, times(1)).addSubscription(any()); } @@ -170,9 +183,11 @@ public class LowLightMonitorTest extends SysuiTestCase { @Test public void testNoSubscribeIfDreamNotPresent() { LowLightMonitor lowLightMonitor = new LowLightMonitor(mLowLightDreamManagerLazy, - mMonitor, mLazyConditions, mScreenLifecycle, mLogger, null, mPackageManager); + mMonitor, mLazyConditions, mScreenLifecycle, mLogger, null, mPackageManager, + mBackgroundExecutor); when(mScreenLifecycle.getScreenState()).thenReturn(SCREEN_ON); lowLightMonitor.start(); + mBackgroundExecutor.runAllReady(); verify(mScreenLifecycle, never()).addObserver(any()); } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt index d073cf1ac9db..c2f0ab92b32b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt @@ -16,8 +16,11 @@ package com.android.systemui.media.controls.ui.view +import android.content.res.Resources import android.testing.TestableLooper import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -25,16 +28,21 @@ import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.plugins.FalsingManager import com.android.systemui.qs.PageIndicator import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock +import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyFloat import org.mockito.Mock import org.mockito.Mockito.anyInt +import org.mockito.Mockito.eq +import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever @SmallTest @TestableLooper.RunWithLooper(setAsMainLooper = true) @@ -42,7 +50,9 @@ import org.mockito.MockitoAnnotations class MediaCarouselScrollHandlerTest : SysuiTestCase() { private val carouselWidth = 1038 + private val settingsButtonWidth = 200 private val motionEventUp = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0f, 0f, 0) + private lateinit var testableLooper: TestableLooper @Mock lateinit var mediaCarousel: MediaScrollView @Mock lateinit var pageIndicator: PageIndicator @@ -53,6 +63,9 @@ class MediaCarouselScrollHandlerTest : SysuiTestCase() { @Mock lateinit var falsingManager: FalsingManager @Mock lateinit var logSmartspaceImpression: (Boolean) -> Unit @Mock lateinit var logger: MediaUiEventLogger + @Mock lateinit var contentContainer: ViewGroup + @Mock lateinit var settingsButton: View + @Mock lateinit var resources: Resources lateinit var executor: FakeExecutor private val clock = FakeSystemClock() @@ -63,6 +76,11 @@ class MediaCarouselScrollHandlerTest : SysuiTestCase() { fun setup() { MockitoAnnotations.initMocks(this) executor = FakeExecutor(clock) + testableLooper = TestableLooper.get(this) + PhysicsAnimatorTestUtils.prepareForTest() + PhysicsAnimatorTestUtils.setAllAnimationsBlock(true) + + whenever(mediaCarousel.contentContainer).thenReturn(contentContainer) mediaCarouselScrollHandler = MediaCarouselScrollHandler( mediaCarousel, @@ -74,13 +92,17 @@ class MediaCarouselScrollHandlerTest : SysuiTestCase() { closeGuts, falsingManager, logSmartspaceImpression, - logger + logger, ) mediaCarouselScrollHandler.playerWidthPlusPadding = carouselWidth - whenever(mediaCarousel.touchListener).thenReturn(mediaCarouselScrollHandler.touchListener) } + @After + fun tearDown() { + PhysicsAnimatorTestUtils.tearDown() + } + @Test fun testCarouselScroll_shortScroll() { whenever(mediaCarousel.isLayoutRtl).thenReturn(false) @@ -128,4 +150,109 @@ class MediaCarouselScrollHandlerTest : SysuiTestCase() { verify(mediaCarousel).smoothScrollTo(eq(0), anyInt()) } + + @Test + fun testCarouselScrollByStep_scrollRight() { + setupMediaContainer(visibleIndex = 0) + + mediaCarouselScrollHandler.scrollByStep(1) + clock.advanceTime(DISMISS_DELAY) + executor.runAllReady() + + verify(mediaCarousel).smoothScrollTo(eq(carouselWidth), anyInt()) + } + + @Test + fun testCarouselScrollByStep_scrollLeft() { + setupMediaContainer(visibleIndex = 1) + + mediaCarouselScrollHandler.scrollByStep(-1) + clock.advanceTime(DISMISS_DELAY) + executor.runAllReady() + + verify(mediaCarousel).smoothScrollTo(eq(0), anyInt()) + } + + @Test + fun testCarouselScrollByStep_scrollRight_alreadyAtEnd() { + setupMediaContainer(visibleIndex = 1) + + mediaCarouselScrollHandler.scrollByStep(1) + clock.advanceTime(DISMISS_DELAY) + executor.runAllReady() + + verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt()) + verify(mediaCarousel).animationTargetX = eq(-settingsButtonWidth.toFloat()) + } + + @Test + fun testCarouselScrollByStep_scrollLeft_alreadyAtStart() { + setupMediaContainer(visibleIndex = 0) + + mediaCarouselScrollHandler.scrollByStep(-1) + clock.advanceTime(DISMISS_DELAY) + executor.runAllReady() + + verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt()) + verify(mediaCarousel).animationTargetX = eq(settingsButtonWidth.toFloat()) + } + + @Test + fun testCarouselScrollByStep_scrollLeft_alreadyAtStart_isRTL() { + setupMediaContainer(visibleIndex = 0) + PhysicsAnimatorTestUtils.setAllAnimationsBlock(true) + whenever(mediaCarousel.isLayoutRtl).thenReturn(true) + + mediaCarouselScrollHandler.scrollByStep(-1) + clock.advanceTime(DISMISS_DELAY) + executor.runAllReady() + + verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt()) + verify(mediaCarousel).animationTargetX = eq(-settingsButtonWidth.toFloat()) + } + + @Test + fun testCarouselScrollByStep_scrollRight_alreadyAtEnd_isRTL() { + setupMediaContainer(visibleIndex = 1) + PhysicsAnimatorTestUtils.setAllAnimationsBlock(true) + whenever(mediaCarousel.isLayoutRtl).thenReturn(true) + + mediaCarouselScrollHandler.scrollByStep(1) + clock.advanceTime(DISMISS_DELAY) + executor.runAllReady() + + verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt()) + verify(mediaCarousel).animationTargetX = eq(settingsButtonWidth.toFloat()) + } + + @Test + fun testScrollByStep_noScroll_notDismissible() { + setupMediaContainer(visibleIndex = 1, showsSettingsButton = false) + + mediaCarouselScrollHandler.scrollByStep(1) + clock.advanceTime(DISMISS_DELAY) + executor.runAllReady() + + verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt()) + verify(mediaCarousel, never()).animationTargetX = anyFloat() + } + + private fun setupMediaContainer(visibleIndex: Int, showsSettingsButton: Boolean = true) { + whenever(contentContainer.childCount).thenReturn(2) + val child1: View = mock() + val child2: View = mock() + whenever(child1.left).thenReturn(0) + whenever(child2.left).thenReturn(carouselWidth) + whenever(contentContainer.getChildAt(0)).thenReturn(child1) + whenever(contentContainer.getChildAt(1)).thenReturn(child2) + + whenever(settingsButton.width).thenReturn(settingsButtonWidth) + whenever(settingsButton.context).thenReturn(context) + whenever(settingsButton.resources).thenReturn(resources) + whenever(settingsButton.resources.getDimensionPixelSize(anyInt())).thenReturn(20) + mediaCarouselScrollHandler.onSettingsButtonUpdated(settingsButton) + + mediaCarouselScrollHandler.visibleMediaIndex = visibleIndex + mediaCarouselScrollHandler.showsSettingsButton = showsSettingsButton + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java index 8e8867b80dc2..847044aa405e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java @@ -166,7 +166,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { mContext.getText(R.string.media_output_dialog_pairing_new).toString()); } - @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING) @Test public void onBindViewHolder_bindGroup_withSessionName_verifyView() { when(mMediaSwitchingController.getSelectedMediaDevice()) @@ -188,7 +188,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE); } - @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING) @Test public void onBindViewHolder_bindGroup_noSessionName_verifyView() { when(mMediaSwitchingController.getSelectedMediaDevice()) @@ -237,7 +237,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE); } - @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING) @Test public void onBindViewHolder_bindConnectedRemoteDevice_verifyView() { when(mMediaSwitchingController.getSelectableMediaDevice()) @@ -818,7 +818,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { verify(mMediaSwitchingController).removeDeviceFromPlayMedia(mMediaDevice1); } - @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING) @Test public void onBindViewHolder_hasVolumeAdjustmentRestriction_verifySeekbarDisabled() { when(mMediaSwitchingController.getSelectedMediaDevice()).thenReturn( @@ -935,7 +935,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { .isEqualTo(R.drawable.media_output_icon_volume); } - @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING) @Test public void multipleSelectedDevices_verifySessionView() { initializeSession(); @@ -956,7 +956,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { assertThat(mViewHolder.mSeekBar.getVolume()).isEqualTo(TEST_CURRENT_VOLUME); } - @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING) @Test public void multipleSelectedDevices_verifyCollapsedView() { initializeSession(); @@ -970,7 +970,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { assertThat(mViewHolder.mEndTouchArea.getVisibility()).isEqualTo(View.GONE); } - @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING) @Test public void multipleSelectedDevices_expandIconClicked_verifyInitialView() { initializeSession(); @@ -993,7 +993,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_1); } - @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING) @Test public void multipleSelectedDevices_expandIconClicked_verifyCollapsedView() { initializeSession(); @@ -1016,7 +1016,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2); } - @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING) @Test public void deviceCanNotBeDeselected_verifyView() { List<MediaDevice> selectedDevices = new ArrayList<>(); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt index 52b9e47e6d3d..52a0a5445002 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt @@ -30,7 +30,7 @@ import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.scene.shared.model.Overlays -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea import com.android.systemui.shade.ui.viewmodel.notificationsShadeOverlayActionsViewModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat @@ -71,13 +71,13 @@ class NotificationsShadeOverlayActionsViewModelTest : SysuiTestCase() { } @Test - fun downFromTopRight_switchesToQuickSettingsShade() = + fun downFromTopEnd_switchesToQuickSettingsShade() = testScope.runTest { val actions by collectLastValue(underTest.actions) underTest.activateIn(this) val action = - (actions?.get(Swipe.Down(fromSource = SceneContainerEdge.TopRight)) as? ShowOverlay) + (actions?.get(Swipe.Down(fromSource = SceneContainerArea.EndHalf)) as? ShowOverlay) assertThat(action?.overlay).isEqualTo(Overlays.QuickSettingsShade) val overlaysToHide = action?.hideCurrentOverlays as? HideCurrentOverlays.Some assertThat(overlaysToHide).isNotNull() 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 26f5d9ea0996..0bba8bba2419 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 @@ -25,6 +25,7 @@ import com.android.systemui.authentication.domain.interactor.AuthenticationResul import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest @@ -65,6 +66,7 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() { fun setUp() { kosmos.sceneContainerStartable.start() kosmos.enableDualShade() + kosmos.runCurrent() underTest.activateIn(testScope) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt index 264eda5a07eb..668c606677ba 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt @@ -25,6 +25,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.TilesUpgradePath import com.android.systemui.settings.userFileManager import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.fakeUserRepository @@ -76,11 +77,11 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { @Test fun setLargeTilesSpecs_inSharedPreferences() { val setA = setOf("tileA", "tileB") - underTest.setLargeTilesSpecs(setA.toTileSpecs()) + underTest.writeLargeTileSpecs(setA.toTileSpecs()) assertThat(getLargeTilesSpecsFromSharedPreferences()).isEqualTo(setA) val setB = setOf("tileA", "tileB") - underTest.setLargeTilesSpecs(setB.toTileSpecs()) + underTest.writeLargeTileSpecs(setB.toTileSpecs()) assertThat(getLargeTilesSpecsFromSharedPreferences()).isEqualTo(setB) } @@ -92,12 +93,12 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) val setA = setOf("tileA", "tileB") - underTest.setLargeTilesSpecs(setA.toTileSpecs()) + underTest.writeLargeTileSpecs(setA.toTileSpecs()) assertThat(getLargeTilesSpecsFromSharedPreferences()).isEqualTo(setA) fakeUserRepository.setSelectedUserInfo(ANOTHER_USER) val setB = setOf("tileA", "tileB") - underTest.setLargeTilesSpecs(setB.toTileSpecs()) + underTest.writeLargeTileSpecs(setB.toTileSpecs()) assertThat(getLargeTilesSpecsFromSharedPreferences()).isEqualTo(setB) fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) @@ -106,7 +107,7 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { } @Test - fun setInitialTilesFromSettings_noLargeTiles_tilesSet() = + fun setUpgradePathFromSettings_noLargeTiles_tilesSet() = with(kosmos) { testScope.runTest { val largeTiles by collectLastValue(underTest.largeTilesSpecs) @@ -117,14 +118,17 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { assertThat(getSharedPreferences().contains(LARGE_TILES_SPECS_KEY)).isFalse() - underTest.setInitialLargeTilesSpecs(tiles, PRIMARY_USER_ID) + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.ReadFromSettings(tiles), + PRIMARY_USER_ID, + ) assertThat(largeTiles).isEqualTo(tiles) } } @Test - fun setInitialTilesFromSettings_alreadyLargeTiles_tilesNotSet() = + fun setUpgradePathFromSettings_alreadyLargeTiles_tilesNotSet() = with(kosmos) { testScope.runTest { val largeTiles by collectLastValue(underTest.largeTilesSpecs) @@ -133,14 +137,17 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { fakeUserRepository.setSelectedUserInfo(ANOTHER_USER) setLargeTilesSpecsInSharedPreferences(setOf("tileC")) - underTest.setInitialLargeTilesSpecs(setOf("tileA").toTileSpecs(), ANOTHER_USER_ID) + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.ReadFromSettings(setOf("tileA").toTileSpecs()), + ANOTHER_USER_ID, + ) assertThat(largeTiles).isEqualTo(setOf("tileC").toTileSpecs()) } } @Test - fun setInitialTilesFromSettings_emptyLargeTiles_tilesNotSet() = + fun setUpgradePathFromSettings_emptyLargeTiles_tilesNotSet() = with(kosmos) { testScope.runTest { val largeTiles by collectLastValue(underTest.largeTilesSpecs) @@ -149,14 +156,17 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { fakeUserRepository.setSelectedUserInfo(ANOTHER_USER) setLargeTilesSpecsInSharedPreferences(emptySet()) - underTest.setInitialLargeTilesSpecs(setOf("tileA").toTileSpecs(), ANOTHER_USER_ID) + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.ReadFromSettings(setOf("tileA").toTileSpecs()), + ANOTHER_USER_ID, + ) assertThat(largeTiles).isEmpty() } } @Test - fun setInitialTilesFromSettings_nonCurrentUser_tilesSetForCorrectUser() = + fun setUpgradePathFromSettings_nonCurrentUser_tilesSetForCorrectUser() = with(kosmos) { testScope.runTest { val largeTiles by collectLastValue(underTest.largeTilesSpecs) @@ -164,7 +174,10 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { fakeUserRepository.setUserInfos(USERS) fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) - underTest.setInitialLargeTilesSpecs(setOf("tileA").toTileSpecs(), ANOTHER_USER_ID) + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.ReadFromSettings(setOf("tileA").toTileSpecs()), + ANOTHER_USER_ID, + ) assertThat(largeTiles).isEqualTo(defaultLargeTilesRepository.defaultLargeTiles) @@ -174,7 +187,7 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { } @Test - fun setInitialTiles_afterDefaultRead_noSetOnRepository_initialTilesCorrect() = + fun setUpgradePath_afterDefaultRead_noSetOnRepository_initialTilesCorrect() = with(kosmos) { testScope.runTest { val largeTiles by collectLastValue(underTest.largeTilesSpecs) @@ -186,14 +199,17 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { assertThat(currentLargeTiles).isNotEmpty() val tiles = setOf("tileA", "tileB") - underTest.setInitialLargeTilesSpecs(tiles.toTileSpecs(), PRIMARY_USER_ID) + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.ReadFromSettings(tiles.toTileSpecs()), + PRIMARY_USER_ID, + ) assertThat(largeTiles).isEqualTo(tiles.toTileSpecs()) } } @Test - fun setInitialTiles_afterDefaultRead_largeTilesSetOnRepository_initialTilesCorrect() = + fun setUpgradePath_afterDefaultRead_largeTilesSetOnRepository_initialTilesCorrect() = with(kosmos) { testScope.runTest { val largeTiles by collectLastValue(underTest.largeTilesSpecs) @@ -204,15 +220,80 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { assertThat(currentLargeTiles).isNotEmpty() - underTest.setLargeTilesSpecs(setOf(TileSpec.create("tileC"))) + underTest.writeLargeTileSpecs(setOf(TileSpec.create("tileC"))) val tiles = setOf("tileA", "tileB") - underTest.setInitialLargeTilesSpecs(tiles.toTileSpecs(), PRIMARY_USER_ID) + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.ReadFromSettings(tiles.toTileSpecs()), + PRIMARY_USER_ID, + ) assertThat(largeTiles).isEqualTo(setOf(TileSpec.create("tileC"))) } } + @Test + fun setTilesRestored_noLargeTiles_tilesSet() = + with(kosmos) { + testScope.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + + fakeUserRepository.setUserInfos(USERS) + fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + val tiles = setOf("tileA", "tileB").toTileSpecs() + + assertThat(getSharedPreferences().contains(LARGE_TILES_SPECS_KEY)).isFalse() + + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.RestoreFromBackup(tiles), + PRIMARY_USER_ID, + ) + + assertThat(largeTiles).isEqualTo(tiles) + } + } + + @Test + fun setDefaultTilesInitial_defaultSetLarge() = + with(kosmos) { + testScope.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + + fakeUserRepository.setUserInfos(USERS) + fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.DefaultSet, + PRIMARY_USER_ID, + ) + + assertThat(largeTiles).isEqualTo(defaultLargeTilesRepository.defaultLargeTiles) + } + } + + @Test + fun setTilesRestored_afterDefaultSet_tilesSet() = + with(kosmos) { + testScope.runTest { + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.DefaultSet, + PRIMARY_USER_ID, + ) + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + + fakeUserRepository.setUserInfos(USERS) + fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + val tiles = setOf("tileA", "tileB").toTileSpecs() + + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.RestoreFromBackup(tiles), + PRIMARY_USER_ID, + ) + + assertThat(largeTiles).isEqualTo(tiles) + } + } + private fun getSharedPreferences(): SharedPreferences = with(kosmos) { return userFileManager.getSharedPreferences( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/LargeTilesUpgradePathsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/LargeTilesUpgradePathsTest.kt new file mode 100644 index 000000000000..f3c1f0c9dba8 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/LargeTilesUpgradePathsTest.kt @@ -0,0 +1,328 @@ +/* + * 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.domain + +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.content.res.mainResources +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.backup.BackupHelper.Companion.ACTION_RESTORE_FINISHED +import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.common.shared.model.PackageChangeModel.Empty.packageName +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.qs.panels.data.repository.QSPreferencesRepository +import com.android.systemui.qs.panels.data.repository.defaultLargeTilesRepository +import com.android.systemui.qs.panels.domain.interactor.qsPreferencesInteractor +import com.android.systemui.qs.pipeline.data.repository.DefaultTilesQSHostRepository +import com.android.systemui.qs.pipeline.data.repository.defaultTilesRepository +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.TilesUpgradePath +import com.android.systemui.settings.userFileManager +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.userRepository +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class LargeTilesUpgradePathsTest : SysuiTestCase() { + + private val kosmos = + testKosmos().apply { defaultTilesRepository = DefaultTilesQSHostRepository(mainResources) } + + private val defaultTiles = kosmos.defaultTilesRepository.defaultTiles.toSet() + + private val underTest = kosmos.qsPreferencesInteractor + + private val Kosmos.userId + get() = userRepository.getSelectedUserInfo().id + + private val Kosmos.intent + get() = + Intent(ACTION_RESTORE_FINISHED).apply { + `package` = packageName + putExtra(Intent.EXTRA_USER_ID, kosmos.userId) + flags = Intent.FLAG_RECEIVER_REGISTERED_ONLY + } + + /** + * This test corresponds to the case of a fresh start. + * + * The resulting large tiles are the default set of large tiles. + */ + @Test + fun defaultTiles_noDataInSharedPreferences_defaultLargeTiles() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + + underTest.setInitialOrUpgradeLargeTilesSpecs(TilesUpgradePath.DefaultSet, userId) + + assertThat(largeTiles).isEqualTo(defaultLargeTilesRepository.defaultLargeTiles) + } + + /** + * This test corresponds to a user that upgraded in place from a build that didn't support large + * tiles to one that does. The current tiles of the user are read from settings. + * + * The resulting large tiles are those that were read from Settings. + */ + @Test + fun upgradeInPlace_noDataInSharedPreferences_allLargeTiles() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + val tiles = setOf("a", "b", "c").toTileSpecs() + + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.ReadFromSettings(tiles), + userId, + ) + + assertThat(largeTiles).isEqualTo(tiles) + } + + /** + * This test corresponds to a fresh start, and then the user restarts the device, without ever + * having modified the set of large tiles. + * + * The resulting large tiles are the default large tiles that were set on the fresh start + */ + @Test + fun defaultSet_restartDevice_largeTilesDontChange() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + + underTest.setInitialOrUpgradeLargeTilesSpecs(TilesUpgradePath.DefaultSet, userId) + + // User restarts the device, this will send a read from settings with the default + // set of tiles + + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.ReadFromSettings(defaultTiles), + userId, + ) + + assertThat(largeTiles).isEqualTo(defaultLargeTilesRepository.defaultLargeTiles) + } + + /** + * This test corresponds to a fresh start, following the user changing the sizes of some tiles. + * After that, the user restarts the device. + * + * The resulting set of large tiles are those that the user determined before restarting the + * device. + */ + @Test + fun defaultSet_someSizeChanges_restart_correctSet() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + underTest.setInitialOrUpgradeLargeTilesSpecs(TilesUpgradePath.DefaultSet, userId) + + underTest.setLargeTilesSpecs(largeTiles!! + setOf("a", "b").toTileSpecs()) + val largeTilesBeforeRestart = largeTiles!! + + // Restart + + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.ReadFromSettings(defaultTiles), + userId, + ) + assertThat(largeTiles).isEqualTo(largeTilesBeforeRestart) + } + + /** + * This test corresponds to a user that upgraded, and after that performed some size changes. + * After that, the user restarts the device. + * + * The resulting set of large tiles are those that the user determined before restarting the + * device. + */ + @Test + fun readFromSettings_changeSizes_restart_newLargeSet() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + val readTiles = setOf("a", "b", "c").toTileSpecs() + + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.ReadFromSettings(readTiles), + userId, + ) + underTest.setLargeTilesSpecs(emptySet()) + + assertThat(largeTiles).isEmpty() + + // Restart + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.ReadFromSettings(readTiles), + userId, + ) + assertThat(largeTiles).isEmpty() + } + + /** + * This test corresponds to a user that upgraded from a build that didn't support tile sizes to + * one that does, via restore from backup. Note that there's no file in SharedPreferences to + * restore. + * + * The resulting set of large tiles are those that were restored from the backup. + */ + @Test + fun restoreFromBackup_noDataInSharedPreferences_allLargeTiles() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + val tiles = setOf("a", "b", "c").toTileSpecs() + + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.RestoreFromBackup(tiles), + userId, + ) + + assertThat(largeTiles).isEqualTo(tiles) + } + + /** + * This test corresponds to a user that upgraded from a build that didn't support tile sizes to + * one that does, via restore from backup. However, the restore happens after SystemUI's + * initialization has set the tiles to default. Note that there's no file in SharedPreferences + * to restore. + * + * The resulting set of large tiles are those that were restored from the backup. + */ + @Test + fun restoreFromBackup_afterDefault_noDataInSharedPreferences_allLargeTiles() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + underTest.setInitialOrUpgradeLargeTilesSpecs(TilesUpgradePath.DefaultSet, userId) + + val tiles = setOf("a", "b", "c").toTileSpecs() + + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.RestoreFromBackup(tiles), + userId, + ) + + assertThat(largeTiles).isEqualTo(tiles) + } + + /** + * This test corresponds to a user that restored from a build that supported different sizes + * tiles. First the list of tiles is restored in Settings and then a file containing some large + * tiles overrides the current shared preferences file + * + * The resulting set of large tiles are those that were restored from the shared preferences + * backup (and not the full list). + */ + @Test + fun restoreFromBackup_thenRestoreOfSharedPrefs_sharedPrefsAreLarge() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + val tiles = setOf("a", "b", "c").toTileSpecs() + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.RestoreFromBackup(tiles), + userId, + ) + + val tilesFromBackupOfSharedPrefs = setOf("a") + setLargeTilesSpecsInSharedPreferences(tilesFromBackupOfSharedPrefs) + broadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent) + + assertThat(largeTiles).isEqualTo(tilesFromBackupOfSharedPrefs.toTileSpecs()) + } + + /** + * This test corresponds to a user that restored from a build that supported different sizes + * tiles. However, this restore of settings happened after SystemUI's restore of the SharedPrefs + * containing the user's previous selections to large/small tiles. + * + * The resulting set of large tiles are those that were restored from the shared preferences + * backup (and not the full list). + */ + @Test + fun restoreFromBackup_afterRestoreOfSharedPrefs_sharedPrefsAreLarge() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + val tiles = setOf("a", "b", "c").toTileSpecs() + val tilesFromBackupOfSharedPrefs = setOf("a") + + setLargeTilesSpecsInSharedPreferences(tilesFromBackupOfSharedPrefs) + broadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent) + + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.RestoreFromBackup(tiles), + userId, + ) + + assertThat(largeTiles).isEqualTo(tilesFromBackupOfSharedPrefs.toTileSpecs()) + } + + /** + * This test corresponds to a user that upgraded from a build that didn't support tile sizes to + * one that does, via restore from backup. After that, the user modifies the size of some tiles + * and then restarts the device. + * + * The resulting set of large tiles are those after the user modifications. + */ + @Test + fun restoreFromBackup_changeSizes_restart_newLargeSet() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + val readTiles = setOf("a", "b", "c").toTileSpecs() + + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.RestoreFromBackup(readTiles), + userId, + ) + underTest.setLargeTilesSpecs(emptySet()) + + assertThat(largeTiles).isEmpty() + + // Restart + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.ReadFromSettings(readTiles), + userId, + ) + assertThat(largeTiles).isEmpty() + } + + private companion object { + private const val LARGE_TILES_SPECS_KEY = "large_tiles_specs" + + private fun Kosmos.getSharedPreferences(): SharedPreferences = + userFileManager.getSharedPreferences( + QSPreferencesRepository.FILE_NAME, + Context.MODE_PRIVATE, + userRepository.getSelectedUserInfo().id, + ) + + private fun Kosmos.setLargeTilesSpecsInSharedPreferences(specs: Set<String>) { + getSharedPreferences().edit().putStringSet(LARGE_TILES_SPECS_KEY, specs).apply() + } + + private fun Kosmos.getLargeTilesSpecsFromSharedPreferences(): Set<String> { + return getSharedPreferences().getStringSet(LARGE_TILES_SPECS_KEY, emptySet())!! + } + + private fun Set<String>.toTileSpecs(): Set<TileSpec> { + return map { TileSpec.create(it) }.toSet() + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt index 79acfdaa415b..9838bcb86684 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt @@ -66,7 +66,7 @@ class IconTilesInteractorTest : SysuiTestCase() { runCurrent() // Resize it to large - qsPreferencesRepository.setLargeTilesSpecs(setOf(spec)) + qsPreferencesRepository.writeLargeTileSpecs(setOf(spec)) runCurrent() // Assert that the new tile was added to the large tiles set diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt index 4b8cd3742bff..d9b3926fa215 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt @@ -24,6 +24,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.TilesUpgradePath import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger import com.android.systemui.res.R import com.android.systemui.retail.data.repository.FakeRetailModeRepository @@ -242,9 +243,12 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() { storeTilesForUser(startingTiles, userId) val tiles by collectLastValue(underTest.tilesSpecs(userId)) - val tilesRead by collectLastValue(underTest.tilesReadFromSetting.consumeAsFlow()) + val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow()) - assertThat(tilesRead).isEqualTo(startingTiles.toTileSpecs().toSet() to userId) + assertThat(tilesRead) + .isEqualTo( + TilesUpgradePath.ReadFromSettings(startingTiles.toTileSpecs().toSet()) to userId + ) } @Test @@ -258,13 +262,13 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() { val tiles10 by collectLastValue(underTest.tilesSpecs(10)) val tiles11 by collectLastValue(underTest.tilesSpecs(11)) - val tilesRead by collectValues(underTest.tilesReadFromSetting.consumeAsFlow()) + val tilesRead by collectValues(underTest.tilesUpgradePath.consumeAsFlow()) assertThat(tilesRead).hasSize(2) assertThat(tilesRead) .containsExactly( - startingTiles10.toTileSpecs().toSet() to 10, - startingTiles11.toTileSpecs().toSet() to 11, + TilesUpgradePath.ReadFromSettings(startingTiles10.toTileSpecs().toSet()) to 10, + TilesUpgradePath.ReadFromSettings(startingTiles11.toTileSpecs().toSet()) to 11, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt index 1945f750efaf..29bd18d3f3a0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt @@ -7,8 +7,8 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.qs.pipeline.data.model.RestoreData -import com.android.systemui.qs.pipeline.data.repository.UserTileSpecRepositoryTest.Companion.toTilesSet import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.TilesUpgradePath import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat @@ -352,11 +352,11 @@ class UserTileSpecRepositoryTest : SysuiTestCase() { @Test fun noSettingsStored_noTilesReadFromSettings() = testScope.runTest { - val tilesRead by collectLastValue(underTest.tilesReadFromSettings.consumeAsFlow()) + val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow()) val tiles by collectLastValue(underTest.tiles()) assertThat(tiles).isEqualTo(getDefaultTileSpecs()) - assertThat(tilesRead).isEqualTo(null) + assertThat(tilesRead).isEqualTo(TilesUpgradePath.DefaultSet) } @Test @@ -365,19 +365,20 @@ class UserTileSpecRepositoryTest : SysuiTestCase() { val storedTiles = "a,b" storeTiles(storedTiles) val tiles by collectLastValue(underTest.tiles()) - val tilesRead by collectLastValue(underTest.tilesReadFromSettings.consumeAsFlow()) + val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow()) - assertThat(tilesRead).isEqualTo(storedTiles.toTilesSet()) + assertThat(tilesRead) + .isEqualTo(TilesUpgradePath.ReadFromSettings(storedTiles.toTilesSet())) } @Test fun noSettingsStored_tilesChanged_tilesReadFromSettingsNotChanged() = testScope.runTest { - val tilesRead by collectLastValue(underTest.tilesReadFromSettings.consumeAsFlow()) + val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow()) val tiles by collectLastValue(underTest.tiles()) underTest.addTile(TileSpec.create("a")) - assertThat(tilesRead).isEqualTo(null) + assertThat(tilesRead).isEqualTo(TilesUpgradePath.DefaultSet) } @Test @@ -386,10 +387,34 @@ class UserTileSpecRepositoryTest : SysuiTestCase() { val storedTiles = "a,b" storeTiles(storedTiles) val tiles by collectLastValue(underTest.tiles()) - val tilesRead by collectLastValue(underTest.tilesReadFromSettings.consumeAsFlow()) + val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow()) underTest.addTile(TileSpec.create("c")) - assertThat(tilesRead).isEqualTo(storedTiles.toTilesSet()) + assertThat(tilesRead) + .isEqualTo(TilesUpgradePath.ReadFromSettings(storedTiles.toTilesSet())) + } + + @Test + fun tilesRestoredFromBackup() = + testScope.runTest { + val specsBeforeRestore = "a,b,c,d,e" + val restoredSpecs = "a,c,d,f" + val autoAddedBeforeRestore = "b,d" + val restoredAutoAdded = "d,e" + + storeTiles(specsBeforeRestore) + val tiles by collectLastValue(underTest.tiles()) + val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow()) + runCurrent() + + val restoreData = + RestoreData(restoredSpecs.toTileSpecs(), restoredAutoAdded.toTilesSet(), USER) + underTest.reconcileRestore(restoreData, autoAddedBeforeRestore.toTilesSet()) + runCurrent() + + val expected = "a,b,c,d,f" + assertThat(tilesRead) + .isEqualTo(TilesUpgradePath.RestoreFromBackup(expected.toTilesSet())) } private fun getDefaultTileSpecs(): List<TileSpec> { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt index df2dd99c779e..b98059a1fe90 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt @@ -31,7 +31,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.qs.panels.ui.viewmodel.editModeViewModel import com.android.systemui.scene.shared.model.Overlays -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest @@ -84,13 +84,14 @@ class QuickSettingsShadeOverlayActionsViewModelTest : SysuiTestCase() { } @Test - fun downFromTopLeft_switchesToNotificationsShade() = + fun downFromTopStart_switchesToNotificationsShade() = testScope.runTest { val actions by collectLastValue(underTest.actions) underTest.activateIn(this) val action = - (actions?.get(Swipe.Down(fromSource = SceneContainerEdge.TopLeft)) as? ShowOverlay) + (actions?.get(Swipe.Down(fromSource = SceneContainerArea.StartHalf)) + as? ShowOverlay) assertThat(action?.overlay).isEqualTo(Overlays.NotificationsShade) val overlaysToHide = action?.hideCurrentOverlays as? HideCurrentOverlays.Some assertThat(overlaysToHide).isNotNull() 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 ec0596515efd..bf5f9f4872f0 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 @@ -25,6 +25,7 @@ import com.android.systemui.authentication.domain.interactor.AuthenticationResul import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest @@ -68,6 +69,7 @@ class QuickSettingsShadeOverlayContentViewModelTest : SysuiTestCase() { fun setUp() { kosmos.sceneContainerStartable.start() kosmos.enableDualShade() + kosmos.runCurrent() underTest.activateIn(testScope) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt index fd485edec117..80c7026b0cea 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt @@ -28,8 +28,10 @@ import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteract import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.scene.data.repository.Idle @@ -44,6 +46,8 @@ import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.fakeSceneDataSource +import com.android.systemui.shade.domain.interactor.disableDualShade +import com.android.systemui.shade.domain.interactor.enableDualShade import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel import com.android.systemui.testKosmos @@ -57,6 +61,10 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify @SmallTest @RunWith(AndroidJUnit4::class) @@ -298,7 +306,9 @@ class SceneInteractorTest : SysuiTestCase() { @Test fun transitioningTo_overlayChange() = - testScope.runTest { + kosmos.runTest { + enableDualShade() + runCurrent() val transitionState = MutableStateFlow<ObservableTransitionState>( ObservableTransitionState.Idle(underTest.currentScene.value) @@ -529,6 +539,8 @@ class SceneInteractorTest : SysuiTestCase() { @Test fun showOverlay_overlayDisabled_doesNothing() = kosmos.runTest { + enableDualShade() + runCurrent() val currentOverlays by collectLastValue(underTest.currentOverlays) val disabledOverlay = Overlays.QuickSettingsShade fakeDisableFlagsRepository.disableFlags.value = @@ -544,6 +556,8 @@ class SceneInteractorTest : SysuiTestCase() { @Test fun replaceOverlay_withDisabledOverlay_doesNothing() = kosmos.runTest { + enableDualShade() + runCurrent() val currentOverlays by collectLastValue(underTest.currentOverlays) val showingOverlay = Overlays.NotificationsShade underTest.showOverlay(showingOverlay, "reason") @@ -618,4 +632,129 @@ class SceneInteractorTest : SysuiTestCase() { // No more active animations, not forced visible. assertThat(isVisible).isFalse() } + + @Test(expected = IllegalStateException::class) + fun changeScene_toIncorrectShade_crashes() = + kosmos.runTest { + enableDualShade() + runCurrent() + underTest.changeScene(Scenes.Shade, "reason") + } + + @Test(expected = IllegalStateException::class) + fun changeScene_toIncorrectQuickSettings_crashes() = + kosmos.runTest { + enableDualShade() + runCurrent() + underTest.changeScene(Scenes.QuickSettings, "reason") + } + + @Test(expected = IllegalStateException::class) + fun snapToScene_toIncorrectShade_crashes() = + kosmos.runTest { + enableDualShade() + runCurrent() + underTest.snapToScene(Scenes.Shade, "reason") + } + + @Test(expected = IllegalStateException::class) + fun snapToScene_toIncorrectQuickSettings_crashes() = + kosmos.runTest { + enableDualShade() + runCurrent() + underTest.changeScene(Scenes.QuickSettings, "reason") + } + + @Test(expected = IllegalStateException::class) + fun showOverlay_incorrectShadeOverlay_crashes() = + kosmos.runTest { + disableDualShade() + runCurrent() + underTest.showOverlay(Overlays.NotificationsShade, "reason") + } + + @Test(expected = IllegalStateException::class) + fun showOverlay_incorrectQuickSettingsOverlay_crashes() = + kosmos.runTest { + disableDualShade() + runCurrent() + underTest.showOverlay(Overlays.QuickSettingsShade, "reason") + } + + @Test + fun instantlyShowOverlay() = + kosmos.runTest { + enableDualShade() + runCurrent() + val currentScene by collectLastValue(underTest.currentScene) + val currentOverlays by collectLastValue(underTest.currentOverlays) + val originalScene = currentScene + assertThat(currentOverlays).isEmpty() + + val overlay = Overlays.NotificationsShade + underTest.instantlyShowOverlay(overlay, "reason") + runCurrent() + + assertThat(currentScene).isEqualTo(originalScene) + assertThat(currentOverlays).contains(overlay) + } + + @Test + fun instantlyHideOverlay() = + kosmos.runTest { + enableDualShade() + runCurrent() + val currentScene by collectLastValue(underTest.currentScene) + val currentOverlays by collectLastValue(underTest.currentOverlays) + val overlay = Overlays.QuickSettingsShade + underTest.showOverlay(overlay, "reason") + runCurrent() + val originalScene = currentScene + assertThat(currentOverlays).contains(overlay) + + underTest.instantlyHideOverlay(overlay, "reason") + runCurrent() + + assertThat(currentScene).isEqualTo(originalScene) + assertThat(currentOverlays).isEmpty() + } + + @Test + fun changeScene_notifiesAboutToChangeListener() = + kosmos.runTest { + val currentScene by collectLastValue(underTest.currentScene) + // Unlock so transitioning to the Gone scene becomes possible. + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + runCurrent() + underTest.changeScene(toScene = Scenes.Gone, loggingReason = "") + runCurrent() + assertThat(currentScene).isEqualTo(Scenes.Gone) + + val processor = mock<SceneInteractor.OnSceneAboutToChangeListener>() + underTest.registerSceneStateProcessor(processor) + + underTest.changeScene( + toScene = Scenes.Lockscreen, + sceneState = KeyguardState.AOD, + loggingReason = "", + ) + runCurrent() + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + + verify(processor).onSceneAboutToChange(Scenes.Lockscreen, KeyguardState.AOD) + } + + @Test + fun changeScene_noOp_whenFromAndToAreTheSame() = + kosmos.runTest { + val currentScene by collectLastValue(underTest.currentScene) + val processor = mock<SceneInteractor.OnSceneAboutToChangeListener>() + underTest.registerSceneStateProcessor(processor) + + underTest.changeScene(toScene = checkNotNull(currentScene), loggingReason = "") + + verify(processor, never()).onSceneAboutToChange(any(), any()) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index 33733103053e..ae77ac4cc327 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -2049,9 +2049,9 @@ class SceneContainerStartableTest : SysuiTestCase() { ) clearInvocations(centralSurfaces) - emulateSceneTransition( + emulateOverlayTransition( transitionStateFlow = transitionStateFlow, - toScene = Scenes.Shade, + toOverlay = Overlays.NotificationsShade, verifyBeforeTransition = { verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) }, @@ -2079,9 +2079,9 @@ class SceneContainerStartableTest : SysuiTestCase() { ) clearInvocations(centralSurfaces) - emulateSceneTransition( + emulateOverlayTransition( transitionStateFlow = transitionStateFlow, - toScene = Scenes.QuickSettings, + toOverlay = Overlays.QuickSettingsShade, verifyBeforeTransition = { verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) }, @@ -2654,22 +2654,36 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test - fun handleDisableFlags() = + fun handleDisableFlags_singleShade() = kosmos.runTest { underTest.start() val currentScene by collectLastValue(sceneInteractor.currentScene) - val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + disableDualShade() runCurrent() sceneInteractor.changeScene(Scenes.Shade, "reason") - sceneInteractor.showOverlay(Overlays.NotificationsShade, "reason") assertThat(currentScene).isEqualTo(Scenes.Shade) - assertThat(currentOverlays).contains(Overlays.NotificationsShade) fakeDisableFlagsRepository.disableFlags.value = DisableFlagsModel(disable2 = StatusBarManager.DISABLE2_NOTIFICATION_SHADE) runCurrent() assertThat(currentScene).isNotEqualTo(Scenes.Shade) + } + + @Test + fun handleDisableFlags_dualShade() = + kosmos.runTest { + underTest.start() + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + enableDualShade() + runCurrent() + sceneInteractor.showOverlay(Overlays.NotificationsShade, "reason") + assertThat(currentOverlays).contains(Overlays.NotificationsShade) + + fakeDisableFlagsRepository.disableFlags.value = + DisableFlagsModel(disable2 = StatusBarManager.DISABLE2_NOTIFICATION_SHADE) + runCurrent() + assertThat(currentOverlays).isEmpty() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetectorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetectorTest.kt new file mode 100644 index 000000000000..a09e5cd9de9b --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetectorTest.kt @@ -0,0 +1,196 @@ +/* + * 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.scene.ui.viewmodel + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.EndEdge +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.EndHalf +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.BottomEdge +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.LeftEdge +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.LeftHalf +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.RightEdge +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.RightHalf +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.StartEdge +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.StartHalf +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SceneContainerSwipeDetectorTest : SysuiTestCase() { + + private val edgeSize = 40 + private val screenWidth = 800 + private val screenHeight = 600 + + private val underTest = SceneContainerSwipeDetector(edgeSize = edgeSize.dp) + + @Test + fun source_noEdge_detectsLeftHalf() { + val detectedEdge = swipeVerticallyFrom(x = screenWidth / 2 - 1, y = screenHeight / 2) + assertThat(detectedEdge).isEqualTo(LeftHalf) + } + + @Test + fun source_swipeVerticallyOnTopLeft_detectsLeftHalf() { + val detectedEdge = swipeVerticallyFrom(x = 1, y = edgeSize - 1) + assertThat(detectedEdge).isEqualTo(LeftHalf) + } + + @Test + fun source_swipeHorizontallyOnTopLeft_detectsLeftEdge() { + val detectedEdge = swipeHorizontallyFrom(x = 1, y = edgeSize - 1) + assertThat(detectedEdge).isEqualTo(LeftEdge) + } + + @Test + fun source_swipeVerticallyOnTopRight_detectsRightHalf() { + val detectedEdge = swipeVerticallyFrom(x = screenWidth - 1, y = edgeSize - 1) + assertThat(detectedEdge).isEqualTo(RightHalf) + } + + @Test + fun source_swipeHorizontallyOnTopRight_detectsRightEdge() { + val detectedEdge = swipeHorizontallyFrom(x = screenWidth - 1, y = edgeSize - 1) + assertThat(detectedEdge).isEqualTo(RightEdge) + } + + @Test + fun source_swipeVerticallyToLeftOfSplit_detectsLeftHalf() { + val detectedEdge = swipeVerticallyFrom(x = (screenWidth / 2) - 1, y = edgeSize - 1) + assertThat(detectedEdge).isEqualTo(LeftHalf) + } + + @Test + fun source_swipeVerticallyToRightOfSplit_detectsRightHalf() { + val detectedEdge = swipeVerticallyFrom(x = (screenWidth / 2) + 1, y = edgeSize - 1) + assertThat(detectedEdge).isEqualTo(RightHalf) + } + + @Test + fun source_swipeVerticallyOnBottom_detectsBottomEdge() { + val detectedEdge = + swipeVerticallyFrom(x = screenWidth / 3, y = screenHeight - (edgeSize / 2)) + assertThat(detectedEdge).isEqualTo(BottomEdge) + } + + @Test + fun source_swipeHorizontallyOnBottom_detectsLeftHalf() { + val detectedEdge = + swipeHorizontallyFrom(x = screenWidth / 3, y = screenHeight - (edgeSize - 1)) + assertThat(detectedEdge).isEqualTo(LeftHalf) + } + + @Test + fun source_swipeHorizontallyOnLeft_detectsLeftEdge() { + val detectedEdge = swipeHorizontallyFrom(x = edgeSize - 1, y = screenHeight / 2) + assertThat(detectedEdge).isEqualTo(LeftEdge) + } + + @Test + fun source_swipeVerticallyOnLeft_detectsLeftHalf() { + val detectedEdge = swipeVerticallyFrom(x = edgeSize - 1, y = screenHeight / 2) + assertThat(detectedEdge).isEqualTo(LeftHalf) + } + + @Test + fun source_swipeHorizontallyOnRight_detectsRightEdge() { + val detectedEdge = + swipeHorizontallyFrom(x = screenWidth - edgeSize + 1, y = screenHeight / 2) + assertThat(detectedEdge).isEqualTo(RightEdge) + } + + @Test + fun source_swipeVerticallyOnRight_detectsRightHalf() { + val detectedEdge = swipeVerticallyFrom(x = screenWidth - edgeSize + 1, y = screenHeight / 2) + assertThat(detectedEdge).isEqualTo(RightHalf) + } + + @Test + fun resolve_startEdgeInLtr_resolvesLeftEdge() { + val resolvedEdge = StartEdge.resolve(LayoutDirection.Ltr) + assertThat(resolvedEdge).isEqualTo(LeftEdge) + } + + @Test + fun resolve_startEdgeInRtl_resolvesRightEdge() { + val resolvedEdge = StartEdge.resolve(LayoutDirection.Rtl) + assertThat(resolvedEdge).isEqualTo(RightEdge) + } + + @Test + fun resolve_endEdgeInLtr_resolvesRightEdge() { + val resolvedEdge = EndEdge.resolve(LayoutDirection.Ltr) + assertThat(resolvedEdge).isEqualTo(RightEdge) + } + + @Test + fun resolve_endEdgeInRtl_resolvesLeftEdge() { + val resolvedEdge = EndEdge.resolve(LayoutDirection.Rtl) + assertThat(resolvedEdge).isEqualTo(LeftEdge) + } + + @Test + fun resolve_startHalfInLtr_resolvesLeftHalf() { + val resolvedEdge = StartHalf.resolve(LayoutDirection.Ltr) + assertThat(resolvedEdge).isEqualTo(LeftHalf) + } + + @Test + fun resolve_startHalfInRtl_resolvesRightHalf() { + val resolvedEdge = StartHalf.resolve(LayoutDirection.Rtl) + assertThat(resolvedEdge).isEqualTo(RightHalf) + } + + @Test + fun resolve_endHalfInLtr_resolvesRightHalf() { + val resolvedEdge = EndHalf.resolve(LayoutDirection.Ltr) + assertThat(resolvedEdge).isEqualTo(RightHalf) + } + + @Test + fun resolve_endHalfInRtl_resolvesLeftHalf() { + val resolvedEdge = EndHalf.resolve(LayoutDirection.Rtl) + assertThat(resolvedEdge).isEqualTo(LeftHalf) + } + + private fun swipeVerticallyFrom(x: Int, y: Int): SceneContainerArea.Resolved? { + return swipeFrom(x, y, Orientation.Vertical) + } + + private fun swipeHorizontallyFrom(x: Int, y: Int): SceneContainerArea.Resolved? { + return swipeFrom(x, y, Orientation.Horizontal) + } + + private fun swipeFrom(x: Int, y: Int, orientation: Orientation): SceneContainerArea.Resolved? { + return underTest.source( + layoutSize = IntSize(width = screenWidth, height = screenHeight), + position = IntOffset(x, y), + density = Density(1f), + orientation = orientation, + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt index 30d9f73d7441..adaebbd27986 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt @@ -48,6 +48,7 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -55,6 +56,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) @EnableSceneContainer @@ -324,7 +326,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { kosmos.enableSingleShade() assertThat(shadeMode).isEqualTo(ShadeMode.Single) - assertThat(underTest.edgeDetector).isEqualTo(DefaultEdgeDetector) + assertThat(underTest.swipeSourceDetector).isEqualTo(DefaultEdgeDetector) } @Test @@ -334,26 +336,28 @@ class SceneContainerViewModelTest : SysuiTestCase() { kosmos.enableSplitShade() assertThat(shadeMode).isEqualTo(ShadeMode.Split) - assertThat(underTest.edgeDetector).isEqualTo(DefaultEdgeDetector) + assertThat(underTest.swipeSourceDetector).isEqualTo(DefaultEdgeDetector) } @Test - fun edgeDetector_dualShade_narrowScreen_usesSplitEdgeDetector() = + fun edgeDetector_dualShade_narrowScreen_usesSceneContainerSwipeDetector() = testScope.runTest { val shadeMode by collectLastValue(kosmos.shadeMode) kosmos.enableDualShade(wideLayout = false) assertThat(shadeMode).isEqualTo(ShadeMode.Dual) - assertThat(underTest.edgeDetector).isEqualTo(kosmos.splitEdgeDetector) + assertThat(underTest.swipeSourceDetector) + .isInstanceOf(SceneContainerSwipeDetector::class.java) } @Test - fun edgeDetector_dualShade_wideScreen_usesSplitEdgeDetector() = + fun edgeDetector_dualShade_wideScreen_usesSceneContainerSwipeDetector() = testScope.runTest { val shadeMode by collectLastValue(kosmos.shadeMode) kosmos.enableDualShade(wideLayout = true) assertThat(shadeMode).isEqualTo(ShadeMode.Dual) - assertThat(underTest.edgeDetector).isEqualTo(kosmos.splitEdgeDetector) + assertThat(underTest.swipeSourceDetector) + .isInstanceOf(SceneContainerSwipeDetector::class.java) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt deleted file mode 100644 index 3d76d280b2cc..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt +++ /dev/null @@ -1,274 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.scene.ui.viewmodel - -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.LayoutDirection -import androidx.compose.ui.unit.dp -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.End -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Bottom -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Left -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Right -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.TopLeft -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.TopRight -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Start -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.TopEnd -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.TopStart -import com.google.common.truth.Truth.assertThat -import kotlin.test.assertFailsWith -import org.junit.Test -import org.junit.runner.RunWith - -@SmallTest -@RunWith(AndroidJUnit4::class) -class SplitEdgeDetectorTest : SysuiTestCase() { - - private val edgeSize = 40 - private val screenWidth = 800 - private val screenHeight = 600 - - private var edgeSplitFraction = 0.7f - - private val underTest = - SplitEdgeDetector( - topEdgeSplitFraction = { edgeSplitFraction }, - edgeSize = edgeSize.dp, - ) - - @Test - fun source_noEdge_detectsNothing() { - val detectedEdge = - swipeVerticallyFrom( - x = screenWidth / 2, - y = screenHeight / 2, - ) - assertThat(detectedEdge).isNull() - } - - @Test - fun source_swipeVerticallyOnTopLeft_detectsTopLeft() { - val detectedEdge = - swipeVerticallyFrom( - x = 1, - y = edgeSize - 1, - ) - assertThat(detectedEdge).isEqualTo(TopLeft) - } - - @Test - fun source_swipeHorizontallyOnTopLeft_detectsLeft() { - val detectedEdge = - swipeHorizontallyFrom( - x = 1, - y = edgeSize - 1, - ) - assertThat(detectedEdge).isEqualTo(Left) - } - - @Test - fun source_swipeVerticallyOnTopRight_detectsTopRight() { - val detectedEdge = - swipeVerticallyFrom( - x = screenWidth - 1, - y = edgeSize - 1, - ) - assertThat(detectedEdge).isEqualTo(TopRight) - } - - @Test - fun source_swipeHorizontallyOnTopRight_detectsRight() { - val detectedEdge = - swipeHorizontallyFrom( - x = screenWidth - 1, - y = edgeSize - 1, - ) - assertThat(detectedEdge).isEqualTo(Right) - } - - @Test - fun source_swipeVerticallyToLeftOfSplit_detectsTopLeft() { - val detectedEdge = - swipeVerticallyFrom( - x = (screenWidth * edgeSplitFraction).toInt() - 1, - y = edgeSize - 1, - ) - assertThat(detectedEdge).isEqualTo(TopLeft) - } - - @Test - fun source_swipeVerticallyToRightOfSplit_detectsTopRight() { - val detectedEdge = - swipeVerticallyFrom( - x = (screenWidth * edgeSplitFraction).toInt() + 1, - y = edgeSize - 1, - ) - assertThat(detectedEdge).isEqualTo(TopRight) - } - - @Test - fun source_edgeSplitFractionUpdatesDynamically() { - val middleX = (screenWidth * 0.5f).toInt() - val topY = 0 - - // Split closer to the right; middle of screen is considered "left". - edgeSplitFraction = 0.6f - assertThat(swipeVerticallyFrom(x = middleX, y = topY)).isEqualTo(TopLeft) - - // Split closer to the left; middle of screen is considered "right". - edgeSplitFraction = 0.4f - assertThat(swipeVerticallyFrom(x = middleX, y = topY)).isEqualTo(TopRight) - - // Illegal fraction. - edgeSplitFraction = 1.2f - assertFailsWith<IllegalArgumentException> { swipeVerticallyFrom(x = middleX, y = topY) } - - // Illegal fraction. - edgeSplitFraction = -0.3f - assertFailsWith<IllegalArgumentException> { swipeVerticallyFrom(x = middleX, y = topY) } - } - - @Test - fun source_swipeVerticallyOnBottom_detectsBottom() { - val detectedEdge = - swipeVerticallyFrom( - x = screenWidth / 3, - y = screenHeight - (edgeSize / 2), - ) - assertThat(detectedEdge).isEqualTo(Bottom) - } - - @Test - fun source_swipeHorizontallyOnBottom_detectsNothing() { - val detectedEdge = - swipeHorizontallyFrom( - x = screenWidth / 3, - y = screenHeight - (edgeSize - 1), - ) - assertThat(detectedEdge).isNull() - } - - @Test - fun source_swipeHorizontallyOnLeft_detectsLeft() { - val detectedEdge = - swipeHorizontallyFrom( - x = edgeSize - 1, - y = screenHeight / 2, - ) - assertThat(detectedEdge).isEqualTo(Left) - } - - @Test - fun source_swipeVerticallyOnLeft_detectsNothing() { - val detectedEdge = - swipeVerticallyFrom( - x = edgeSize - 1, - y = screenHeight / 2, - ) - assertThat(detectedEdge).isNull() - } - - @Test - fun source_swipeHorizontallyOnRight_detectsRight() { - val detectedEdge = - swipeHorizontallyFrom( - x = screenWidth - edgeSize + 1, - y = screenHeight / 2, - ) - assertThat(detectedEdge).isEqualTo(Right) - } - - @Test - fun source_swipeVerticallyOnRight_detectsNothing() { - val detectedEdge = - swipeVerticallyFrom( - x = screenWidth - edgeSize + 1, - y = screenHeight / 2, - ) - assertThat(detectedEdge).isNull() - } - - @Test - fun resolve_startInLtr_resolvesLeft() { - val resolvedEdge = Start.resolve(LayoutDirection.Ltr) - assertThat(resolvedEdge).isEqualTo(Left) - } - - @Test - fun resolve_startInRtl_resolvesRight() { - val resolvedEdge = Start.resolve(LayoutDirection.Rtl) - assertThat(resolvedEdge).isEqualTo(Right) - } - - @Test - fun resolve_endInLtr_resolvesRight() { - val resolvedEdge = End.resolve(LayoutDirection.Ltr) - assertThat(resolvedEdge).isEqualTo(Right) - } - - @Test - fun resolve_endInRtl_resolvesLeft() { - val resolvedEdge = End.resolve(LayoutDirection.Rtl) - assertThat(resolvedEdge).isEqualTo(Left) - } - - @Test - fun resolve_topStartInLtr_resolvesTopLeft() { - val resolvedEdge = TopStart.resolve(LayoutDirection.Ltr) - assertThat(resolvedEdge).isEqualTo(TopLeft) - } - - @Test - fun resolve_topStartInRtl_resolvesTopRight() { - val resolvedEdge = TopStart.resolve(LayoutDirection.Rtl) - assertThat(resolvedEdge).isEqualTo(TopRight) - } - - @Test - fun resolve_topEndInLtr_resolvesTopRight() { - val resolvedEdge = TopEnd.resolve(LayoutDirection.Ltr) - assertThat(resolvedEdge).isEqualTo(TopRight) - } - - @Test - fun resolve_topEndInRtl_resolvesTopLeft() { - val resolvedEdge = TopEnd.resolve(LayoutDirection.Rtl) - assertThat(resolvedEdge).isEqualTo(TopLeft) - } - - private fun swipeVerticallyFrom(x: Int, y: Int): SceneContainerEdge.Resolved? { - return swipeFrom(x, y, Orientation.Vertical) - } - - private fun swipeHorizontallyFrom(x: Int, y: Int): SceneContainerEdge.Resolved? { - return swipeFrom(x, y, Orientation.Horizontal) - } - - private fun swipeFrom(x: Int, y: Int, orientation: Orientation): SceneContainerEdge.Resolved? { - return underTest.source( - layoutSize = IntSize(width = screenWidth, height = screenHeight), - position = IntOffset(x, y), - density = Density(1f), - orientation = orientation, - ) - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/policy/WorkProfilePolicyTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/policy/WorkProfilePolicyTest.kt index c1477fe52f0e..a9f3a655ada9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/policy/WorkProfilePolicyTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/policy/WorkProfilePolicyTest.kt @@ -80,6 +80,8 @@ class WorkProfilePolicyTest { // Set desktop mode supported whenever(mContext.resources).thenReturn(mResources) whenever(mResources.getBoolean(R.bool.config_isDesktopModeSupported)).thenReturn(true) + whenever(mResources.getBoolean(R.bool.config_canInternalDisplayHostDesktops)) + .thenReturn(true) policy = WorkProfilePolicy(kosmos.profileTypeRepository, mContext) } 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 79fc999e1b50..904f5e869637 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -87,6 +87,7 @@ 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.keyguard.domain.interactor.NaturalScrollingSettingObserver; +import com.android.systemui.keyguard.ui.transitions.BlurConfig; import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel; import com.android.systemui.kosmos.KosmosJavaAdapter; @@ -587,7 +588,8 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mPowerInteractor, mKeyguardClockPositionAlgorithm, mMSDLPlayer, - mBrightnessMirrorShowingInteractor); + mBrightnessMirrorShowingInteractor, + new BlurConfig(0f, 0f)); mNotificationPanelViewController.initDependencies( mCentralSurfaces, null, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt index 508836e3b48b..4a011c0844e5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt @@ -781,28 +781,6 @@ class ShadeInteractorSceneContainerImplTest : SysuiTestCase() { } @Test - fun collapseQuickSettingsShadeNotBypassingShade_splitShade_switchesToLockscreen() = - testScope.runTest { - kosmos.enableSplitShade() - val shadeMode by collectLastValue(kosmos.shadeMode) - val currentScene by collectLastValue(sceneInteractor.currentScene) - val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) - assertThat(shadeMode).isEqualTo(ShadeMode.Split) - - sceneInteractor.changeScene(Scenes.QuickSettings, "reason") - assertThat(currentScene).isEqualTo(Scenes.QuickSettings) - assertThat(currentOverlays).isEmpty() - - underTest.collapseQuickSettingsShade( - loggingReason = "reason", - bypassNotificationsShade = false, - ) - - assertThat(currentScene).isEqualTo(Scenes.Lockscreen) - assertThat(currentOverlays).isEmpty() - } - - @Test fun collapseQuickSettingsShadeBypassingShade_singleShade_switchesToLockscreen() = testScope.runTest { kosmos.enableSingleShade() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/BlurUtilsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/BlurUtilsTest.kt index e7b6e4d34fe8..402b53c12bda 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/BlurUtilsTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/BlurUtilsTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar +import android.content.res.Resources import android.view.CrossWindowBlurListeners import android.view.SurfaceControl import android.view.ViewRootImpl @@ -46,6 +47,7 @@ class BlurUtilsTest : SysuiTestCase() { @Mock lateinit var dumpManager: DumpManager @Mock lateinit var transaction: SurfaceControl.Transaction @Mock lateinit var crossWindowBlurListeners: CrossWindowBlurListeners + @Mock lateinit var resources: Resources lateinit var blurUtils: TestableBlurUtils @Before @@ -109,7 +111,7 @@ class BlurUtilsTest : SysuiTestCase() { verify(transaction).setEarlyWakeupEnd() } - inner class TestableBlurUtils : BlurUtils(blurConfig, crossWindowBlurListeners, dumpManager) { + inner class TestableBlurUtils : BlurUtils(resources, blurConfig, crossWindowBlurListeners, dumpManager) { var blursEnabled = true override fun supportsBlursOnWindows(): Boolean { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java index 3d3178793a09..c6801f1ad9d5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java @@ -460,14 +460,14 @@ public class CommandQueueTest extends SysuiTestCase { } @Test - public void testonDisplayAddSystemDecorations() { + public void testOnDisplayAddSystemDecorations() { mCommandQueue.onDisplayAddSystemDecorations(DEFAULT_DISPLAY); waitForIdleSync(); verify(mCallbacks).onDisplayAddSystemDecorations(eq(DEFAULT_DISPLAY)); } @Test - public void testonDisplayAddSystemDecorationsForSecondaryDisplay() { + public void testOnDisplayAddSystemDecorationsForSecondaryDisplay() { mCommandQueue.onDisplayAddSystemDecorations(SECONDARY_DISPLAY); waitForIdleSync(); verify(mCallbacks).onDisplayAddSystemDecorations(eq(SECONDARY_DISPLAY)); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt index dbe8f8226d43..c7b3175a636f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt @@ -31,7 +31,6 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.activityStarter import com.android.systemui.res.R import com.android.systemui.statusbar.StatusBarIconView -import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer @@ -265,91 +264,25 @@ class CallChipViewModelTest : SysuiTestCase() { } @Test - fun chip_positiveStartTime_notPromoted_colorsAreThemed() = + fun chip_positiveStartTime_colorsAreAccentThemed() = testScope.runTest { val latest by collectLastValue(underTest.chip) repo.setOngoingCallState(inCallModel(startTimeMs = 1000, promotedContent = null)) assertThat((latest as OngoingActivityChipModel.Active).colors) - .isEqualTo(ColorsModel.Themed) + .isEqualTo(ColorsModel.AccentThemed) } @Test - fun chip_zeroStartTime_notPromoted_colorsAreThemed() = + fun chip_zeroStartTime_colorsAreAccentThemed() = testScope.runTest { val latest by collectLastValue(underTest.chip) repo.setOngoingCallState(inCallModel(startTimeMs = 0, promotedContent = null)) assertThat((latest as OngoingActivityChipModel.Active).colors) - .isEqualTo(ColorsModel.Themed) - } - - @Test - @DisableFlags(StatusBarNotifChips.FLAG_NAME) - fun chip_positiveStartTime_promoted_notifChipsFlagOff_colorsAreThemed() = - testScope.runTest { - val latest by collectLastValue(underTest.chip) - - repo.setOngoingCallState( - inCallModel(startTimeMs = 1000, promotedContent = PROMOTED_CONTENT_WITH_COLOR) - ) - - assertThat((latest as OngoingActivityChipModel.Active).colors) - .isEqualTo(ColorsModel.Themed) - } - - @Test - @DisableFlags(StatusBarNotifChips.FLAG_NAME) - fun chip_zeroStartTime_promoted_notifChipsFlagOff_colorsAreThemed() = - testScope.runTest { - val latest by collectLastValue(underTest.chip) - - repo.setOngoingCallState( - inCallModel(startTimeMs = 0, promotedContent = PROMOTED_CONTENT_WITH_COLOR) - ) - - assertThat((latest as OngoingActivityChipModel.Active).colors) - .isEqualTo(ColorsModel.Themed) - } - - @Test - @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun chip_positiveStartTime_promoted_notifChipsFlagOn_colorsAreCustom() = - testScope.runTest { - val latest by collectLastValue(underTest.chip) - - repo.setOngoingCallState( - inCallModel(startTimeMs = 1000, promotedContent = PROMOTED_CONTENT_WITH_COLOR) - ) - - assertThat((latest as OngoingActivityChipModel.Active).colors) - .isEqualTo( - ColorsModel.Custom( - backgroundColorInt = PROMOTED_BACKGROUND_COLOR, - primaryTextColorInt = PROMOTED_PRIMARY_TEXT_COLOR, - ) - ) - } - - @Test - @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun chip_zeroStartTime_promoted_notifChipsFlagOff_colorsAreCustom() = - testScope.runTest { - val latest by collectLastValue(underTest.chip) - - repo.setOngoingCallState( - inCallModel(startTimeMs = 0, promotedContent = PROMOTED_CONTENT_WITH_COLOR) - ) - - assertThat((latest as OngoingActivityChipModel.Active).colors) - .isEqualTo( - ColorsModel.Custom( - backgroundColorInt = PROMOTED_BACKGROUND_COLOR, - primaryTextColorInt = PROMOTED_PRIMARY_TEXT_COLOR, - ) - ) + .isEqualTo(ColorsModel.AccentThemed) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt index 192ad879891f..aaa9b58a45df 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt @@ -186,7 +186,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { @Test @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) - fun chips_onePromotedNotif_colorMatches() = + fun chips_onePromotedNotif_colorIsSystemThemed() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -209,10 +209,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { ) assertThat(latest).hasSize(1) - val colors = latest!![0].colors - assertThat(colors).isInstanceOf(ColorsModel.Custom::class.java) - assertThat((colors as ColorsModel.Custom).backgroundColorInt).isEqualTo(56) - assertThat((colors as ColorsModel.Custom).primaryTextColorInt).isEqualTo(89) + assertThat(latest!![0].colors).isEqualTo(ColorsModel.SystemThemed) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/view/ChipTextTruncationHelperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/view/ChipTextTruncationHelperTest.kt index d727089094f0..9ec5a42714bf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/view/ChipTextTruncationHelperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/view/ChipTextTruncationHelperTest.kt @@ -52,24 +52,13 @@ class ChipTextTruncationHelperTest : SysuiTestCase() { } @Test - fun shouldShowText_desiredSlightlyLargerThanMax_true() { + fun shouldShowText_desiredMoreThanMax_false() { val result = underTest.shouldShowText( desiredTextWidthPx = (MAX_WIDTH * 1.1).toInt(), widthMeasureSpec = UNLIMITED_WIDTH_SPEC, ) - assertThat(result).isTrue() - } - - @Test - fun shouldShowText_desiredMoreThanTwiceMax_false() { - val result = - underTest.shouldShowText( - desiredTextWidthPx = (MAX_WIDTH * 2.2).toInt(), - widthMeasureSpec = UNLIMITED_WIDTH_SPEC, - ) - assertThat(result).isFalse() } @@ -80,8 +69,8 @@ class ChipTextTruncationHelperTest : SysuiTestCase() { View.MeasureSpec.makeMeasureSpec(MAX_WIDTH / 2, View.MeasureSpec.AT_MOST) ) - // WHEN desired is more than twice the smallerWidthSpec - val desiredWidth = (MAX_WIDTH * 1.1).toInt() + // WHEN desired is more than the smallerWidthSpec + val desiredWidth = ((MAX_WIDTH / 2) * 1.1).toInt() val result = underTest.shouldShowText( @@ -100,8 +89,8 @@ class ChipTextTruncationHelperTest : SysuiTestCase() { View.MeasureSpec.makeMeasureSpec(MAX_WIDTH * 3, View.MeasureSpec.AT_MOST) ) - // WHEN desired is more than twice the max - val desiredWidth = (MAX_WIDTH * 2.2).toInt() + // WHEN desired is more than the max + val desiredWidth = (MAX_WIDTH * 1.1).toInt() val result = underTest.shouldShowText( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt index 60030ad4e428..e3a84fd2c2eb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt @@ -54,7 +54,7 @@ class ChipTransitionHelperTest : SysuiTestCase() { OngoingActivityChipModel.Active.Timer( key = KEY, icon = createIcon(R.drawable.ic_cake), - colors = ColorsModel.Themed, + colors = ColorsModel.AccentThemed, startTimeMs = 100L, onClickListenerLegacy = null, clickBehavior = OngoingActivityChipModel.ClickBehavior.None, @@ -68,7 +68,7 @@ class ChipTransitionHelperTest : SysuiTestCase() { OngoingActivityChipModel.Active.IconOnly( key = KEY, icon = createIcon(R.drawable.ic_hotspot), - colors = ColorsModel.Themed, + colors = ColorsModel.AccentThemed, onClickListenerLegacy = null, clickBehavior = OngoingActivityChipModel.ClickBehavior.None, ) @@ -90,7 +90,7 @@ class ChipTransitionHelperTest : SysuiTestCase() { OngoingActivityChipModel.Active.Timer( key = KEY, icon = createIcon(R.drawable.ic_cake), - colors = ColorsModel.Themed, + colors = ColorsModel.AccentThemed, startTimeMs = 100L, onClickListenerLegacy = null, clickBehavior = OngoingActivityChipModel.ClickBehavior.None, @@ -132,7 +132,7 @@ class ChipTransitionHelperTest : SysuiTestCase() { OngoingActivityChipModel.Active.Timer( key = KEY, icon = createIcon(R.drawable.ic_cake), - colors = ColorsModel.Themed, + colors = ColorsModel.AccentThemed, startTimeMs = 100L, onClickListenerLegacy = null, clickBehavior = OngoingActivityChipModel.ClickBehavior.None, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt index 816df0102940..20637cd4af33 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt @@ -61,14 +61,14 @@ import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.notification.data.model.activeNotificationModel import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository +import com.android.systemui.statusbar.notification.data.repository.addNotif +import com.android.systemui.statusbar.notification.data.repository.addNotifs import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel -import com.android.systemui.statusbar.notification.shared.CallType import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization -import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository -import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel -import com.android.systemui.statusbar.phone.ongoingcall.shared.model.inCallModel +import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState +import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.removeOngoingCallState import com.android.systemui.testKosmos import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat @@ -93,7 +93,6 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { private val screenRecordState = kosmos.screenRecordRepository.screenRecordState private val mediaProjectionState = kosmos.fakeMediaProjectionRepository.mediaProjectionState - private val callRepo = kosmos.ongoingCallRepository private val activeNotificationListRepository = kosmos.activeNotificationListRepository private val mockSystemUIDialog = mock<SystemUIDialog>() @@ -132,7 +131,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { kosmos.runTest { screenRecordState.value = ScreenRecordModel.DoingNothing mediaProjectionState.value = MediaProjectionState.NotProjecting - callRepo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState(key = "call") val latest by collectLastValue(underTest.primaryChip) @@ -145,7 +144,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { kosmos.runTest { screenRecordState.value = ScreenRecordModel.DoingNothing mediaProjectionState.value = MediaProjectionState.NotProjecting - callRepo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState(key = "call") val latest by collectLastValue(underTest.chipsLegacy) val unused by collectLastValue(underTest.chips) @@ -178,7 +177,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording mediaProjectionState.value = MediaProjectionState.NotProjecting - callRepo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState(key = "call") val latest by collectLastValue(underTest.primaryChip) @@ -191,7 +190,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording mediaProjectionState.value = MediaProjectionState.NotProjecting - callRepo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState(key = "call") val latest by collectLastValue(underTest.chipsLegacy) val unused by collectLastValue(underTest.chips) @@ -224,7 +223,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { fun primaryChip_screenRecordShowAndCallShow_screenRecordShown() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34)) + addOngoingCallState("call") val latest by collectLastValue(underTest.primaryChip) @@ -237,9 +236,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { kosmos.runTest { val callNotificationKey = "call" screenRecordState.value = ScreenRecordModel.Recording - callRepo.setOngoingCallState( - inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) - ) + addOngoingCallState(callNotificationKey) val latest by collectLastValue(underTest.chipsLegacy) val unused by collectLastValue(underTest.chips) @@ -255,16 +252,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { kosmos.runTest { val callNotificationKey = "call" screenRecordState.value = ScreenRecordModel.Recording - setNotifs( - listOf( - activeNotificationModel( - key = "call", - statusBarChipIcon = createStatusBarIconViewOrNull(), - callType = CallType.Ongoing, - whenTime = 499, - ) - ) - ) + addOngoingCallState(callNotificationKey) val latest by collectLastValue(underTest.chips) val unused by collectLastValue(underTest.chipsLegacy) @@ -281,7 +269,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { @Test fun chipsLegacy_oneChip_notSquished() = kosmos.runTest { - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call")) + addOngoingCallState() val latest by collectLastValue(underTest.chipsLegacy) @@ -294,17 +282,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { @Test fun chips_oneChip_notSquished() = kosmos.runTest { - val callNotificationKey = "call" - setNotifs( - listOf( - activeNotificationModel( - key = callNotificationKey, - statusBarChipIcon = createStatusBarIconViewOrNull(), - callType = CallType.Ongoing, - whenTime = 499, - ) - ) - ) + addOngoingCallState() val latest by collectLastValue(underTest.chips) @@ -315,10 +293,10 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) @Test - fun chipsLegacy_twoTimerChips_isSmallPortrait_andChipsModernizationDisabled_bothSquished() = + fun chipsLegacy_twoTimerChips_isSmallPortrait_bothSquished() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call")) + addOngoingCallState(key = "call") val latest by collectLastValue(underTest.chipsLegacy) @@ -329,12 +307,28 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) } + @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + @Test + fun chips_twoTimerChips_isSmallPortrait_bothSquished() = + kosmos.runTest { + screenRecordState.value = ScreenRecordModel.Recording + addOngoingCallState(key = "call") + + val latest by collectLastValue(underTest.chips) + + // Squished chips are icon only + assertThat(latest!!.active[0]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + assertThat(latest!!.active[1]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + } + @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) @Test fun chipsLegacy_countdownChipAndTimerChip_countdownNotSquished_butTimerSquished() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Starting(millisUntilStarted = 2000) - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call")) + addOngoingCallState(key = "call") val latest by collectLastValue(underTest.chipsLegacy) @@ -346,6 +340,23 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) } + @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + @Test + fun chips_countdownChipAndTimerChip_countdownNotSquished_butTimerSquished() = + kosmos.runTest { + screenRecordState.value = ScreenRecordModel.Starting(millisUntilStarted = 2000) + addOngoingCallState(key = "call") + + val latest by collectLastValue(underTest.chips) + + // The screen record countdown isn't squished to icon-only + assertThat(latest!!.active[0]) + .isInstanceOf(OngoingActivityChipModel.Active.Countdown::class.java) + // But the call chip *is* squished + assertThat(latest!!.active[1]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + } + @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) @Test fun chipsLegacy_numberOfChipsChanges_chipsGetSquishedAndUnsquished() = @@ -354,7 +365,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { // WHEN there's only one chip screenRecordState.value = ScreenRecordModel.Recording - callRepo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState(key = "call") // The screen record isn't squished because it's the only one assertThat(latest!!.primary) @@ -363,7 +374,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Inactive::class.java) // WHEN there's 2 chips - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call")) + addOngoingCallState(key = "call") // THEN they both become squished assertThat(latest!!.primary) @@ -382,12 +393,44 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Inactive::class.java) } + @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + @Test + fun chips_numberOfChipsChanges_chipsGetSquishedAndUnsquished() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + + // WHEN there's only one chip + screenRecordState.value = ScreenRecordModel.Recording + removeOngoingCallState(key = "call") + + // The screen record isn't squished because it's the only one + assertThat(latest!!.active[0]) + .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) + + // WHEN there's 2 chips + addOngoingCallState(key = "call") + + // THEN they both become squished + assertThat(latest!!.active[0]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + // But the call chip *is* squished + assertThat(latest!!.active[1]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + + // WHEN we go back down to 1 chip + screenRecordState.value = ScreenRecordModel.DoingNothing + + // THEN the remaining chip unsquishes + assertThat(latest!!.active[0]) + .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) + } + @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) @Test fun chipsLegacy_twoChips_isLandscape_notSquished() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call")) + addOngoingCallState(key = "call") // WHEN we're in landscape val config = @@ -405,12 +448,35 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) } + @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + @Test + fun chips_twoChips_isLandscape_notSquished() = + kosmos.runTest { + screenRecordState.value = ScreenRecordModel.Recording + addOngoingCallState(key = "call") + + // WHEN we're in landscape + val config = + Configuration(kosmos.mainResources.configuration).apply { + orientation = Configuration.ORIENTATION_LANDSCAPE + } + kosmos.fakeConfigurationRepository.onConfigurationChange(config) + + val latest by collectLastValue(underTest.chips) + + // THEN the chips aren't squished (squished chips would be icon only) + assertThat(latest!!.active[0]) + .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) + assertThat(latest!!.active[1]) + .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) + } + @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) @Test fun chipsLegacy_twoChips_isLargeScreen_notSquished() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call")) + addOngoingCallState(key = "call") // WHEN we're on a large screen kosmos.displayStateRepository.setIsLargeScreen(true) @@ -424,25 +490,19 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) } - @Test @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) - fun chips_twoChips_chipsModernizationEnabled_notSquished() = + @Test + fun chips_twoChips_isLargeScreen_notSquished() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording - setNotifs( - listOf( - activeNotificationModel( - key = "call", - statusBarChipIcon = createStatusBarIconViewOrNull(), - callType = CallType.Ongoing, - whenTime = 499, - ) - ) - ) + addOngoingCallState(key = "call") + + // WHEN we're on a large screen + kosmos.displayStateRepository.setIsLargeScreen(true) val latest by collectLastValue(underTest.chips) - // Squished chips would be icon only + // THEN the chips aren't squished (squished chips would be icon only) assertThat(latest!!.active[0]) .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) assertThat(latest!!.active[1]) @@ -455,7 +515,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { screenRecordState.value = ScreenRecordModel.Recording mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) - callRepo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState(key = "call") val latest by collectLastValue(underTest.primaryChip) @@ -469,7 +529,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { screenRecordState.value = ScreenRecordModel.Recording mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) - callRepo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState(key = "call") val latest by collectLastValue(underTest.chipsLegacy) val unused by collectLastValue(underTest.chips) @@ -510,7 +570,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { screenRecordState.value = ScreenRecordModel.DoingNothing mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34)) + addOngoingCallState(key = "call") val latest by collectLastValue(underTest.primaryChip) @@ -525,9 +585,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { screenRecordState.value = ScreenRecordModel.DoingNothing mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) - callRepo.setOngoingCallState( - inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) - ) + addOngoingCallState(key = "call") val latest by collectLastValue(underTest.chipsLegacy) val unused by collectLastValue(underTest.chips) @@ -545,16 +603,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { screenRecordState.value = ScreenRecordModel.DoingNothing mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) - setNotifs( - listOf( - activeNotificationModel( - key = callNotificationKey, - statusBarChipIcon = createStatusBarIconViewOrNull(), - callType = CallType.Ongoing, - whenTime = 499, - ) - ) - ) + addOngoingCallState(key = callNotificationKey) val latest by collectLastValue(underTest.chips) val unused by collectLastValue(underTest.chipsLegacy) @@ -575,9 +624,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { mediaProjectionState.value = MediaProjectionState.NotProjecting val callNotificationKey = "call" - callRepo.setOngoingCallState( - inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) - ) + addOngoingCallState(key = callNotificationKey) val latest by collectLastValue(underTest.primaryChip) @@ -593,9 +640,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { // MediaProjection covers both share-to-app and cast-to-other-device mediaProjectionState.value = MediaProjectionState.NotProjecting - callRepo.setOngoingCallState( - inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) - ) + addOngoingCallState(key = callNotificationKey) val latest by collectLastValue(underTest.chipsLegacy) val unused by collectLastValue(underTest.chips) @@ -614,16 +659,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { screenRecordState.value = ScreenRecordModel.DoingNothing // MediaProjection covers both share-to-app and cast-to-other-device mediaProjectionState.value = MediaProjectionState.NotProjecting - setNotifs( - listOf( - activeNotificationModel( - key = callNotificationKey, - statusBarChipIcon = createStatusBarIconViewOrNull(), - callType = CallType.Ongoing, - whenTime = 499, - ) - ) - ) + addOngoingCallState(key = callNotificationKey) val latest by collectLastValue(underTest.chips) val unused by collectLastValue(underTest.chipsLegacy) @@ -837,12 +873,10 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val unused by collectLastValue(underTest.chips) val callNotificationKey = "call" - callRepo.setOngoingCallState( - inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) - ) + addOngoingCallState(callNotificationKey) val firstIcon = createStatusBarIconViewOrNull() - setNotifs( + activeNotificationListRepository.addNotifs( listOf( activeNotificationModel( key = "firstNotif", @@ -874,14 +908,10 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val callNotificationKey = "call" val firstIcon = createStatusBarIconViewOrNull() val secondIcon = createStatusBarIconViewOrNull() - setNotifs( + addOngoingCallState(key = callNotificationKey) + activeNotificationListRepository.addNotifs( listOf( activeNotificationModel( - key = callNotificationKey, - whenTime = 499, - callType = CallType.Ongoing, - ), - activeNotificationModel( key = "firstNotif", statusBarChipIcon = firstIcon, promotedContent = @@ -913,17 +943,13 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val latest by collectLastValue(underTest.chipsLegacy) val unused by collectLastValue(underTest.chips) - callRepo.setOngoingCallState( - inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) - ) + addOngoingCallState(callNotificationKey) screenRecordState.value = ScreenRecordModel.Recording - setNotifs( - listOf( - activeNotificationModel( - key = "notif", - statusBarChipIcon = createStatusBarIconViewOrNull(), - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), - ) + activeNotificationListRepository.addNotif( + activeNotificationModel( + key = "notif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = PromotedNotificationContentModel.Builder("notif").build(), ) ) @@ -942,20 +968,14 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val callNotificationKey = "call" val notifIcon = createStatusBarIconViewOrNull() screenRecordState.value = ScreenRecordModel.Recording - setNotifs( - listOf( - activeNotificationModel( - key = callNotificationKey, - whenTime = 499, - callType = CallType.Ongoing, - ), - activeNotificationModel( - key = "notif", - statusBarChipIcon = notifIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), - ), + activeNotificationListRepository.addNotif( + activeNotificationModel( + key = "notif", + statusBarChipIcon = notifIcon, + promotedContent = PromotedNotificationContentModel.Builder("notif").build(), ) ) + addOngoingCallState(key = callNotificationKey) assertThat(latest!!.active.size).isEqualTo(2) assertIsScreenRecordChip(latest!!.active[0]) @@ -982,7 +1002,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { ) ) // And everything else hidden - callRepo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState(key = callNotificationKey) mediaProjectionState.value = MediaProjectionState.NotProjecting screenRecordState.value = ScreenRecordModel.DoingNothing @@ -991,9 +1011,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertIsNotifChip(latest, context, notifIcon, "notif") // WHEN the higher priority call chip is added - callRepo.setOngoingCallState( - inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) - ) + addOngoingCallState(callNotificationKey) // THEN the higher priority call chip is used assertIsCallChip(latest, callNotificationKey) @@ -1024,17 +1042,13 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { screenRecordState.value = ScreenRecordModel.Recording mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) - callRepo.setOngoingCallState( - inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) - ) + addOngoingCallState(callNotificationKey) val notifIcon = createStatusBarIconViewOrNull() - setNotifs( - listOf( - activeNotificationModel( - key = "notif", - statusBarChipIcon = notifIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), - ) + activeNotificationListRepository.addNotif( + activeNotificationModel( + key = "notif", + statusBarChipIcon = notifIcon, + promotedContent = PromotedNotificationContentModel.Builder("notif").build(), ) ) @@ -1056,7 +1070,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertIsCallChip(latest, callNotificationKey) // WHEN the higher priority call is removed - callRepo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState(key = callNotificationKey) // THEN the lower priority notif is used assertIsNotifChip(latest, context, notifIcon, "notif") @@ -1069,17 +1083,15 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val callNotificationKey = "call" // Start with just the lowest priority chip shown val notifIcon = createStatusBarIconViewOrNull() - setNotifs( - listOf( - activeNotificationModel( - key = "notif", - statusBarChipIcon = notifIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), - ) + activeNotificationListRepository.addNotif( + activeNotificationModel( + key = "notif", + statusBarChipIcon = notifIcon, + promotedContent = PromotedNotificationContentModel.Builder("notif").build(), ) ) // And everything else hidden - callRepo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState(key = callNotificationKey) mediaProjectionState.value = MediaProjectionState.NotProjecting screenRecordState.value = ScreenRecordModel.DoingNothing @@ -1092,9 +1104,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel()) // WHEN the higher priority call chip is added - callRepo.setOngoingCallState( - inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) - ) + addOngoingCallState(callNotificationKey) // THEN the higher priority call chip is used as primary and notif is demoted to // secondary @@ -1125,7 +1135,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { // WHEN screen record and call is dropped screenRecordState.value = ScreenRecordModel.DoingNothing - callRepo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState(key = callNotificationKey) // THEN media projection and notif remain assertIsShareToAppChip(latest!!.primary) @@ -1172,21 +1182,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) // WHEN the higher priority call chip is added - setNotifs( - listOf( - activeNotificationModel( - key = callNotificationKey, - statusBarChipIcon = createStatusBarIconViewOrNull(), - callType = CallType.Ongoing, - whenTime = 499, - ), - activeNotificationModel( - key = "notif", - statusBarChipIcon = notifIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), - ), - ) - ) + addOngoingCallState(key = callNotificationKey) // THEN the higher priority call chip and notif are active in that order assertThat(latest!!.active.size).isEqualTo(2) @@ -1372,7 +1368,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording mediaProjectionState.value = MediaProjectionState.NotProjecting - callRepo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState(key = "call") val latest by collectLastValue(underTest.primaryChip) @@ -1399,7 +1395,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) screenRecordState.value = ScreenRecordModel.DoingNothing - callRepo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState(key = "call") val latest by collectLastValue(underTest.primaryChip) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java deleted file mode 100644 index a64339e20f7c..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java +++ /dev/null @@ -1,1189 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.systemui.statusbar.notification.row; - -import static android.app.Notification.EXTRA_BUILDER_APPLICATION_INFO; -import static android.app.NotificationChannel.SOCIAL_MEDIA_ID; -import static android.app.NotificationChannel.USER_LOCKED_IMPORTANCE; -import static android.app.NotificationManager.IMPORTANCE_DEFAULT; -import static android.app.NotificationManager.IMPORTANCE_LOW; -import static android.app.NotificationManager.IMPORTANCE_MIN; -import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; -import static android.print.PrintManager.PRINT_SPOOLER_PACKAGE_NAME; -import static android.view.View.GONE; -import static android.view.View.VISIBLE; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertTrue; - -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.anyBoolean; -import static org.mockito.Mockito.anyInt; -import static org.mockito.Mockito.anyString; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.app.Flags; -import android.app.INotificationManager; -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationChannelGroup; -import android.app.PendingIntent; -import android.app.Person; -import android.content.ComponentName; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.graphics.drawable.Drawable; -import android.os.RemoteException; -import android.os.UserHandle; -import android.platform.test.annotations.DisableFlags; -import android.platform.test.annotations.EnableFlags; -import android.platform.test.flag.junit.SetFlagsRule; -import android.service.notification.StatusBarNotification; -import android.telecom.TelecomManager; -import android.testing.TestableLooper; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.internal.logging.MetricsLogger; -import com.android.internal.logging.testing.UiEventLoggerFake; -import com.android.systemui.Dependency; -import com.android.systemui.SysuiTestCase; -import com.android.systemui.res.R; -import com.android.systemui.statusbar.RankingBuilder; -import com.android.systemui.statusbar.notification.AssistantFeedbackController; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; - -import java.util.List; -import java.util.Optional; -import java.util.concurrent.CountDownLatch; - -@SmallTest -@RunWith(AndroidJUnit4.class) -@TestableLooper.RunWithLooper -public class NotificationInfoTest extends SysuiTestCase { - private static final String TEST_PACKAGE_NAME = "test_package"; - private static final String TEST_SYSTEM_PACKAGE_NAME = PRINT_SPOOLER_PACKAGE_NAME; - private static final int TEST_UID = 1; - private static final String TEST_CHANNEL = "test_channel"; - private static final String TEST_CHANNEL_NAME = "TEST CHANNEL NAME"; - - private TestableLooper mTestableLooper; - private NotificationInfo mNotificationInfo; - private NotificationChannel mNotificationChannel; - private NotificationChannel mDefaultNotificationChannel; - private NotificationChannel mClassifiedNotificationChannel; - private StatusBarNotification mSbn; - private NotificationEntry mEntry; - private UiEventLoggerFake mUiEventLogger = new UiEventLoggerFake(); - - @Rule - public MockitoRule mockito = MockitoJUnit.rule(); - @Mock - private MetricsLogger mMetricsLogger; - @Mock - private INotificationManager mMockINotificationManager; - @Mock - private PackageManager mMockPackageManager; - @Mock - private OnUserInteractionCallback mOnUserInteractionCallback; - @Mock - private ChannelEditorDialogController mChannelEditorDialogController; - @Mock - private AssistantFeedbackController mAssistantFeedbackController; - @Mock - private TelecomManager mTelecomManager; - - @Rule - public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - - @Before - public void setUp() throws Exception { - mTestableLooper = TestableLooper.get(this); - - mContext.addMockSystemService(TelecomManager.class, mTelecomManager); - - mDependency.injectTestDependency(Dependency.BG_LOOPER, mTestableLooper.getLooper()); - // Inflate the layout - final LayoutInflater layoutInflater = LayoutInflater.from(mContext); - mNotificationInfo = (NotificationInfo) layoutInflater.inflate(R.layout.notification_info, - null); - mNotificationInfo.setGutsParent(mock(NotificationGuts.class)); - // Our view is never attached to a window so the View#post methods in NotificationInfo never - // get called. Setting this will skip the post and do the action immediately. - mNotificationInfo.mSkipPost = true; - - // PackageManager must return a packageInfo and applicationInfo. - final PackageInfo packageInfo = new PackageInfo(); - packageInfo.packageName = TEST_PACKAGE_NAME; - when(mMockPackageManager.getPackageInfo(eq(TEST_PACKAGE_NAME), anyInt())) - .thenReturn(packageInfo); - final ApplicationInfo applicationInfo = new ApplicationInfo(); - applicationInfo.uid = TEST_UID; // non-zero - final PackageInfo systemPackageInfo = new PackageInfo(); - systemPackageInfo.packageName = TEST_SYSTEM_PACKAGE_NAME; - when(mMockPackageManager.getPackageInfo(eq(TEST_SYSTEM_PACKAGE_NAME), anyInt())) - .thenReturn(systemPackageInfo); - when(mMockPackageManager.getPackageInfo(eq("android"), anyInt())) - .thenReturn(packageInfo); - - ComponentName assistant = new ComponentName("package", "service"); - when(mMockINotificationManager.getAllowedNotificationAssistant()).thenReturn(assistant); - ResolveInfo ri = new ResolveInfo(); - ri.activityInfo = new ActivityInfo(); - ri.activityInfo.packageName = assistant.getPackageName(); - ri.activityInfo.name = "activity"; - when(mMockPackageManager.queryIntentActivities(any(), anyInt())).thenReturn(List.of(ri)); - - // Package has one channel by default. - when(mMockINotificationManager.getNumNotificationChannelsForPackage( - eq(TEST_PACKAGE_NAME), eq(TEST_UID), anyBoolean())).thenReturn(1); - - // Some test channels. - mNotificationChannel = new NotificationChannel( - TEST_CHANNEL, TEST_CHANNEL_NAME, IMPORTANCE_LOW); - mDefaultNotificationChannel = new NotificationChannel( - NotificationChannel.DEFAULT_CHANNEL_ID, TEST_CHANNEL_NAME, - IMPORTANCE_LOW); - mClassifiedNotificationChannel = - new NotificationChannel(SOCIAL_MEDIA_ID, "social", IMPORTANCE_LOW); - - Notification notification = new Notification(); - notification.extras.putParcelable(EXTRA_BUILDER_APPLICATION_INFO, applicationInfo); - mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, 0, - notification, UserHandle.getUserHandleForUid(TEST_UID), null, 0); - mEntry = new NotificationEntryBuilder().setSbn(mSbn).build(); - when(mAssistantFeedbackController.isFeedbackEnabled()).thenReturn(false); - when(mAssistantFeedbackController.getInlineDescriptionResource(any())) - .thenReturn(R.string.notification_channel_summary_automatic); - } - - private void doStandardBind() throws Exception { - mNotificationInfo.bindNotification( - mMockPackageManager, - mMockINotificationManager, - mOnUserInteractionCallback, - mChannelEditorDialogController, - TEST_PACKAGE_NAME, - mNotificationChannel, - mEntry, - null, - null, - null, - mUiEventLogger, - true, - false, - true, - mAssistantFeedbackController, - mMetricsLogger, null); - } - - @Test - public void testBindNotification_SetsTextApplicationName() throws Exception { - when(mMockPackageManager.getApplicationLabel(any())).thenReturn("App Name"); - doStandardBind(); - final TextView textView = mNotificationInfo.findViewById(R.id.pkg_name); - assertTrue(textView.getText().toString().contains("App Name")); - assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.header).getVisibility()); - } - - @Test - public void testBindNotification_SetsPackageIcon() throws Exception { - final Drawable iconDrawable = mock(Drawable.class); - when(mMockPackageManager.getApplicationIcon(any(ApplicationInfo.class))) - .thenReturn(iconDrawable); - doStandardBind(); - final ImageView iconView = mNotificationInfo.findViewById(R.id.pkg_icon); - assertEquals(iconDrawable, iconView.getDrawable()); - } - - @Test - public void testBindNotification_noDelegate() throws Exception { - doStandardBind(); - final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name); - assertEquals(GONE, nameView.getVisibility()); - } - - @Test - public void testBindNotification_delegate() throws Exception { - mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, "other", 0, null, TEST_UID, 0, - new Notification(), UserHandle.CURRENT, null, 0); - final ApplicationInfo applicationInfo = new ApplicationInfo(); - applicationInfo.uid = 7; // non-zero - when(mMockPackageManager.getApplicationInfo(eq("other"), anyInt())).thenReturn( - applicationInfo); - when(mMockPackageManager.getApplicationLabel(any())).thenReturn("Other"); - - NotificationEntry entry = new NotificationEntryBuilder().setSbn(mSbn).build(); - mNotificationInfo.bindNotification( - mMockPackageManager, - mMockINotificationManager, - mOnUserInteractionCallback, - mChannelEditorDialogController, - TEST_PACKAGE_NAME, - mNotificationChannel, - entry, - null, - null, - null, - mUiEventLogger, - true, - false, - true, - mAssistantFeedbackController, - mMetricsLogger, null); - final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name); - assertEquals(VISIBLE, nameView.getVisibility()); - assertTrue(nameView.getText().toString().contains("Proxied")); - } - - @Test - public void testBindNotification_GroupNameHiddenIfNoGroup() throws Exception { - doStandardBind(); - final TextView groupNameView = mNotificationInfo.findViewById(R.id.group_name); - assertEquals(GONE, groupNameView.getVisibility()); - } - - @Test - public void testBindNotification_SetsGroupNameIfNonNull() throws Exception { - mNotificationChannel.setGroup("test_group_id"); - final NotificationChannelGroup notificationChannelGroup = - new NotificationChannelGroup("test_group_id", "Test Group Name"); - when(mMockINotificationManager.getNotificationChannelGroupForPackage( - eq("test_group_id"), eq(TEST_PACKAGE_NAME), eq(TEST_UID))) - .thenReturn(notificationChannelGroup); - doStandardBind(); - final TextView groupNameView = mNotificationInfo.findViewById(R.id.group_name); - assertEquals(View.VISIBLE, groupNameView.getVisibility()); - assertEquals("Test Group Name", groupNameView.getText()); - } - - @Test - public void testBindNotification_SetsTextChannelName() throws Exception { - doStandardBind(); - final TextView textView = mNotificationInfo.findViewById(R.id.channel_name); - assertEquals(TEST_CHANNEL_NAME, textView.getText()); - } - - @Test - public void testBindNotification_DefaultChannelDoesNotUseChannelName() throws Exception { - mNotificationInfo.bindNotification( - mMockPackageManager, - mMockINotificationManager, - mOnUserInteractionCallback, - mChannelEditorDialogController, - TEST_PACKAGE_NAME, - mDefaultNotificationChannel, - mEntry, - null, - null, - null, - mUiEventLogger, - true, - false, - true, - mAssistantFeedbackController, - mMetricsLogger, null); - final TextView textView = mNotificationInfo.findViewById(R.id.channel_name); - assertEquals(GONE, textView.getVisibility()); - } - - @Test - public void testBindNotification_DefaultChannelUsesChannelNameIfMoreChannelsExist() - throws Exception { - // Package has more than one channel by default. - when(mMockINotificationManager.getNumNotificationChannelsForPackage( - eq(TEST_PACKAGE_NAME), eq(TEST_UID), anyBoolean())).thenReturn(10); - mNotificationInfo.bindNotification( - mMockPackageManager, - mMockINotificationManager, - mOnUserInteractionCallback, - mChannelEditorDialogController, - TEST_PACKAGE_NAME, - mDefaultNotificationChannel, - mEntry, - null, - null, - null, - mUiEventLogger, - true, - false, - true, - mAssistantFeedbackController, - mMetricsLogger, - null); - final TextView textView = mNotificationInfo.findViewById(R.id.channel_name); - assertEquals(VISIBLE, textView.getVisibility()); - } - - @Test - public void testBindNotification_UnblockablePackageUsesChannelName() throws Exception { - mNotificationInfo.bindNotification( - mMockPackageManager, - mMockINotificationManager, - mOnUserInteractionCallback, - mChannelEditorDialogController, - TEST_PACKAGE_NAME, - mNotificationChannel, - mEntry, - null, - null, - null, - mUiEventLogger, - true, - true, - true, - mAssistantFeedbackController, - mMetricsLogger, null); - final TextView textView = mNotificationInfo.findViewById(R.id.channel_name); - assertEquals(VISIBLE, textView.getVisibility()); - } - - @Test - public void testBindNotification_SetsOnClickListenerForSettings() throws Exception { - final CountDownLatch latch = new CountDownLatch(1); - mNotificationInfo.bindNotification( - mMockPackageManager, - mMockINotificationManager, - mOnUserInteractionCallback, - mChannelEditorDialogController, - TEST_PACKAGE_NAME, - mNotificationChannel, - mEntry, - (View v, NotificationChannel c, int appUid) -> { - assertEquals(mNotificationChannel, c); - latch.countDown(); - }, - null, - null, - mUiEventLogger, - true, - false, - true, - mAssistantFeedbackController, - mMetricsLogger, - null); - - final View settingsButton = mNotificationInfo.findViewById(R.id.info); - settingsButton.performClick(); - // Verify that listener was triggered. - assertEquals(0, latch.getCount()); - } - - @Test - public void testBindNotification_SettingsButtonInvisibleWhenNoClickListener() throws Exception { - doStandardBind(); - final View settingsButton = mNotificationInfo.findViewById(R.id.info); - assertTrue(settingsButton.getVisibility() != View.VISIBLE); - } - - @Test - public void testBindNotification_SettingsButtonInvisibleWhenDeviceUnprovisioned() - throws Exception { - mNotificationInfo.bindNotification( - mMockPackageManager, - mMockINotificationManager, - mOnUserInteractionCallback, - mChannelEditorDialogController, - TEST_PACKAGE_NAME, - mNotificationChannel, - mEntry, - (View v, NotificationChannel c, int appUid) -> { - assertEquals(mNotificationChannel, c); - }, - null, - null, - mUiEventLogger, - false, - false, - true, - mAssistantFeedbackController, - mMetricsLogger, - null); - final View settingsButton = mNotificationInfo.findViewById(R.id.info); - assertTrue(settingsButton.getVisibility() != View.VISIBLE); - } - - @Test - public void testBindNotification_SettingsButtonReappearsAfterSecondBind() throws Exception { - doStandardBind(); - mNotificationInfo.bindNotification( - mMockPackageManager, - mMockINotificationManager, - mOnUserInteractionCallback, - mChannelEditorDialogController, - TEST_PACKAGE_NAME, - mNotificationChannel, - mEntry, - (View v, NotificationChannel c, int appUid) -> { }, - null, - null, - mUiEventLogger, - true, - false, - true, - mAssistantFeedbackController, - mMetricsLogger, - null); - final View settingsButton = mNotificationInfo.findViewById(R.id.info); - assertEquals(View.VISIBLE, settingsButton.getVisibility()); - } - - @Test - public void testBindNotification_whenAppUnblockable() throws Exception { - mNotificationInfo.bindNotification( - mMockPackageManager, - mMockINotificationManager, - mOnUserInteractionCallback, - mChannelEditorDialogController, - TEST_PACKAGE_NAME, - mNotificationChannel, - mEntry, - null, - null, - null, - mUiEventLogger, - true, - true, - true, - mAssistantFeedbackController, - mMetricsLogger, null); - final TextView view = mNotificationInfo.findViewById(R.id.non_configurable_text); - assertEquals(View.VISIBLE, view.getVisibility()); - assertEquals(mContext.getString(R.string.notification_unblockable_desc), - view.getText()); - assertEquals(GONE, - mNotificationInfo.findViewById(R.id.interruptiveness_settings).getVisibility()); - } - - @Test - public void testBindNotification_whenCurrentlyInCall() throws Exception { - when(mMockINotificationManager.isInCall(anyString(), anyInt())).thenReturn(true); - - Person person = new Person.Builder() - .setName("caller") - .build(); - Notification.Builder nb = new Notification.Builder( - mContext, mNotificationChannel.getId()) - .setContentTitle("foo") - .setSmallIcon(android.R.drawable.sym_def_app_icon) - .setStyle(Notification.CallStyle.forOngoingCall( - person, mock(PendingIntent.class))) - .setFullScreenIntent(mock(PendingIntent.class), true) - .addAction(new Notification.Action.Builder(null, "test", null).build()); - - mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, 0, - nb.build(), UserHandle.getUserHandleForUid(TEST_UID), null, 0); - mEntry.setSbn(mSbn); - doStandardBind(); - final TextView view = mNotificationInfo.findViewById(R.id.non_configurable_call_text); - assertEquals(View.VISIBLE, view.getVisibility()); - assertEquals(mContext.getString(R.string.notification_unblockable_call_desc), - view.getText()); - assertEquals(GONE, - mNotificationInfo.findViewById(R.id.interruptiveness_settings).getVisibility()); - assertEquals(GONE, - mNotificationInfo.findViewById(R.id.non_configurable_text).getVisibility()); - } - - @Test - public void testBindNotification_whenCurrentlyInCall_notCall() throws Exception { - when(mMockINotificationManager.isInCall(anyString(), anyInt())).thenReturn(true); - - Notification.Builder nb = new Notification.Builder( - mContext, mNotificationChannel.getId()) - .setContentTitle("foo") - .setSmallIcon(android.R.drawable.sym_def_app_icon) - .setFullScreenIntent(mock(PendingIntent.class), true) - .addAction(new Notification.Action.Builder(null, "test", null).build()); - - mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, 0, - nb.build(), UserHandle.getUserHandleForUid(TEST_UID), null, 0); - mEntry.setSbn(mSbn); - doStandardBind(); - assertEquals(GONE, - mNotificationInfo.findViewById(R.id.non_configurable_call_text).getVisibility()); - assertEquals(VISIBLE, - mNotificationInfo.findViewById(R.id.interruptiveness_settings).getVisibility()); - assertEquals(GONE, - mNotificationInfo.findViewById(R.id.non_configurable_text).getVisibility()); - } - - @Test - public void testBindNotification_automaticIsVisible() throws Exception { - when(mAssistantFeedbackController.isFeedbackEnabled()).thenReturn(true); - doStandardBind(); - assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.automatic).getVisibility()); - assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.automatic_summary).getVisibility()); - } - - @Test - public void testBindNotification_automaticIsGone() throws Exception { - doStandardBind(); - assertEquals(GONE, mNotificationInfo.findViewById(R.id.automatic).getVisibility()); - assertEquals(GONE, mNotificationInfo.findViewById(R.id.automatic_summary).getVisibility()); - } - - @Test - public void testBindNotification_automaticIsSelected() throws Exception { - when(mAssistantFeedbackController.isFeedbackEnabled()).thenReturn(true); - mNotificationChannel.unlockFields(USER_LOCKED_IMPORTANCE); - doStandardBind(); - assertTrue(mNotificationInfo.findViewById(R.id.automatic).isSelected()); - } - - @Test - public void testBindNotification_alertIsSelected() throws Exception { - doStandardBind(); - assertTrue(mNotificationInfo.findViewById(R.id.alert).isSelected()); - } - - @Test - public void testBindNotification_silenceIsSelected() throws Exception { - mNotificationInfo.bindNotification( - mMockPackageManager, - mMockINotificationManager, - mOnUserInteractionCallback, - mChannelEditorDialogController, - TEST_PACKAGE_NAME, - mNotificationChannel, - mEntry, - null, - null, - null, - mUiEventLogger, - true, - false, - false, - mAssistantFeedbackController, - mMetricsLogger, - null); - assertTrue(mNotificationInfo.findViewById(R.id.silence).isSelected()); - } - - @Test - public void testBindNotification_DoesNotUpdateNotificationChannel() throws Exception { - doStandardBind(); - mTestableLooper.processAllMessages(); - verify(mMockINotificationManager, never()).updateNotificationChannelForPackage( - anyString(), eq(TEST_UID), any()); - } - - @Test - public void testBindNotification_LogsOpen() throws Exception { - doStandardBind(); - assertEquals(1, mUiEventLogger.numLogs()); - assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.getId(), - mUiEventLogger.eventId(0)); - } - - @Test - public void testDoesNotUpdateNotificationChannelAfterImportanceChanged() throws Exception { - mNotificationChannel.setImportance(IMPORTANCE_LOW); - mNotificationInfo.bindNotification( - mMockPackageManager, - mMockINotificationManager, - mOnUserInteractionCallback, - mChannelEditorDialogController, - TEST_PACKAGE_NAME, - mNotificationChannel, - mEntry, - null, - null, - null, - mUiEventLogger, - true, - false, - false, - mAssistantFeedbackController, - mMetricsLogger, - null); - - mNotificationInfo.findViewById(R.id.alert).performClick(); - mTestableLooper.processAllMessages(); - verify(mMockINotificationManager, never()).updateNotificationChannelForPackage( - anyString(), eq(TEST_UID), any()); - } - - @Test - public void testDoesNotUpdateNotificationChannelAfterImportanceChangedSilenced() - throws Exception { - mNotificationChannel.setImportance(IMPORTANCE_DEFAULT); - doStandardBind(); - - mNotificationInfo.findViewById(R.id.silence).performClick(); - mTestableLooper.processAllMessages(); - verify(mMockINotificationManager, never()).updateNotificationChannelForPackage( - anyString(), eq(TEST_UID), any()); - } - - @Test - public void testDoesNotUpdateNotificationChannelAfterImportanceChangedAutomatic() - throws Exception { - mNotificationChannel.setImportance(IMPORTANCE_DEFAULT); - doStandardBind(); - - mNotificationInfo.findViewById(R.id.automatic).performClick(); - mTestableLooper.processAllMessages(); - verify(mMockINotificationManager, never()).updateNotificationChannelForPackage( - anyString(), eq(TEST_UID), any()); - } - - @Test - public void testHandleCloseControls_persistAutomatic() - throws Exception { - when(mAssistantFeedbackController.isFeedbackEnabled()).thenReturn(true); - mNotificationChannel.unlockFields(USER_LOCKED_IMPORTANCE); - doStandardBind(); - - mNotificationInfo.handleCloseControls(true, false); - mTestableLooper.processAllMessages(); - verify(mMockINotificationManager, times(1)).unlockNotificationChannel( - anyString(), eq(TEST_UID), any()); - } - - @Test - public void testHandleCloseControls_DoesNotUpdateNotificationChannelIfUnchanged() - throws Exception { - int originalImportance = mNotificationChannel.getImportance(); - doStandardBind(); - - mNotificationInfo.handleCloseControls(true, false); - mTestableLooper.processAllMessages(); - verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage( - anyString(), eq(TEST_UID), any()); - assertEquals(originalImportance, mNotificationChannel.getImportance()); - - assertEquals(2, mUiEventLogger.numLogs()); - assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.getId(), - mUiEventLogger.eventId(0)); - // The SAVE_IMPORTANCE event is logged whenever importance is saved, even if unchanged. - assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_SAVE_IMPORTANCE.getId(), - mUiEventLogger.eventId(1)); - } - - @Test - public void testHandleCloseControls_DoesNotUpdateNotificationChannelIfUnspecified() - throws Exception { - mNotificationChannel.setImportance(IMPORTANCE_UNSPECIFIED); - doStandardBind(); - - mNotificationInfo.handleCloseControls(true, false); - - mTestableLooper.processAllMessages(); - verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage( - anyString(), eq(TEST_UID), any()); - assertEquals(IMPORTANCE_UNSPECIFIED, mNotificationChannel.getImportance()); - } - - @Test - public void testSilenceCallsUpdateNotificationChannel() throws Exception { - mNotificationChannel.setImportance(IMPORTANCE_DEFAULT); - doStandardBind(); - - mNotificationInfo.findViewById(R.id.silence).performClick(); - mNotificationInfo.findViewById(R.id.done).performClick(); - mNotificationInfo.handleCloseControls(true, false); - - mTestableLooper.processAllMessages(); - ArgumentCaptor<NotificationChannel> updated = - ArgumentCaptor.forClass(NotificationChannel.class); - verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage( - anyString(), eq(TEST_UID), updated.capture()); - assertTrue((updated.getValue().getUserLockedFields() - & USER_LOCKED_IMPORTANCE) != 0); - assertEquals(IMPORTANCE_LOW, updated.getValue().getImportance()); - - assertEquals(2, mUiEventLogger.numLogs()); - assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.getId(), - mUiEventLogger.eventId(0)); - assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_SAVE_IMPORTANCE.getId(), - mUiEventLogger.eventId(1)); - assertFalse(mNotificationInfo.shouldBeSavedOnClose()); - } - - @Test - public void testUnSilenceCallsUpdateNotificationChannel() throws Exception { - mNotificationChannel.setImportance(IMPORTANCE_LOW); - mNotificationInfo.bindNotification( - mMockPackageManager, - mMockINotificationManager, - mOnUserInteractionCallback, - mChannelEditorDialogController, - TEST_PACKAGE_NAME, - mNotificationChannel, - mEntry, - null, - null, - null, - mUiEventLogger, - true, - false, - false, - mAssistantFeedbackController, - mMetricsLogger, - null); - - mNotificationInfo.findViewById(R.id.alert).performClick(); - mNotificationInfo.findViewById(R.id.done).performClick(); - mNotificationInfo.handleCloseControls(true, false); - - mTestableLooper.processAllMessages(); - ArgumentCaptor<NotificationChannel> updated = - ArgumentCaptor.forClass(NotificationChannel.class); - verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage( - anyString(), eq(TEST_UID), updated.capture()); - assertTrue((updated.getValue().getUserLockedFields() - & USER_LOCKED_IMPORTANCE) != 0); - assertEquals(IMPORTANCE_DEFAULT, updated.getValue().getImportance()); - assertFalse(mNotificationInfo.shouldBeSavedOnClose()); - } - - @Test - public void testAutomaticUnlocksUserImportance() throws Exception { - when(mAssistantFeedbackController.isFeedbackEnabled()).thenReturn(true); - mNotificationChannel.setImportance(IMPORTANCE_DEFAULT); - mNotificationChannel.lockFields(USER_LOCKED_IMPORTANCE); - doStandardBind(); - - mNotificationInfo.findViewById(R.id.automatic).performClick(); - mNotificationInfo.findViewById(R.id.done).performClick(); - mNotificationInfo.handleCloseControls(true, false); - - mTestableLooper.processAllMessages(); - verify(mMockINotificationManager, times(1)).unlockNotificationChannel( - anyString(), eq(TEST_UID), any()); - assertEquals(IMPORTANCE_DEFAULT, mNotificationChannel.getImportance()); - assertFalse(mNotificationInfo.shouldBeSavedOnClose()); - } - - @Test - public void testSilenceCallsUpdateNotificationChannel_channelImportanceUnspecified() - throws Exception { - mNotificationChannel.setImportance(IMPORTANCE_UNSPECIFIED); - doStandardBind(); - - mNotificationInfo.findViewById(R.id.silence).performClick(); - mNotificationInfo.findViewById(R.id.done).performClick(); - mNotificationInfo.handleCloseControls(true, false); - - mTestableLooper.processAllMessages(); - ArgumentCaptor<NotificationChannel> updated = - ArgumentCaptor.forClass(NotificationChannel.class); - verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage( - anyString(), eq(TEST_UID), updated.capture()); - assertTrue((updated.getValue().getUserLockedFields() - & USER_LOCKED_IMPORTANCE) != 0); - assertEquals(IMPORTANCE_LOW, updated.getValue().getImportance()); - assertFalse(mNotificationInfo.shouldBeSavedOnClose()); - } - - @Test - public void testSilenceCallsUpdateNotificationChannel_channelImportanceMin() - throws Exception { - mNotificationChannel.setImportance(IMPORTANCE_MIN); - mNotificationInfo.bindNotification( - mMockPackageManager, - mMockINotificationManager, - mOnUserInteractionCallback, - mChannelEditorDialogController, - TEST_PACKAGE_NAME, - mNotificationChannel, - mEntry, - null, - null, - null, - mUiEventLogger, - true, - false, - false, - mAssistantFeedbackController, - mMetricsLogger, - null); - - assertEquals(mContext.getString(R.string.inline_done_button), - ((TextView) mNotificationInfo.findViewById(R.id.done)).getText()); - mNotificationInfo.findViewById(R.id.silence).performClick(); - assertEquals(mContext.getString(R.string.inline_done_button), - ((TextView) mNotificationInfo.findViewById(R.id.done)).getText()); - mNotificationInfo.findViewById(R.id.done).performClick(); - mNotificationInfo.handleCloseControls(true, false); - - mTestableLooper.processAllMessages(); - ArgumentCaptor<NotificationChannel> updated = - ArgumentCaptor.forClass(NotificationChannel.class); - verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage( - anyString(), eq(TEST_UID), updated.capture()); - assertTrue((updated.getValue().getUserLockedFields() & USER_LOCKED_IMPORTANCE) != 0); - assertEquals(IMPORTANCE_MIN, updated.getValue().getImportance()); - assertFalse(mNotificationInfo.shouldBeSavedOnClose()); - } - - @Test - public void testSilence_closeGutsThenTryToSave() throws RemoteException { - mNotificationChannel.setImportance(IMPORTANCE_DEFAULT); - mNotificationInfo.bindNotification( - mMockPackageManager, - mMockINotificationManager, - mOnUserInteractionCallback, - mChannelEditorDialogController, - TEST_PACKAGE_NAME, - mNotificationChannel, - mEntry, - null, - null, - null, - mUiEventLogger, - true, - false, - false, - mAssistantFeedbackController, - mMetricsLogger, - null); - - mNotificationInfo.findViewById(R.id.silence).performClick(); - mNotificationInfo.handleCloseControls(false, false); - mNotificationInfo.handleCloseControls(true, false); - - mTestableLooper.processAllMessages(); - - assertEquals(IMPORTANCE_DEFAULT, mNotificationChannel.getImportance()); - assertFalse(mNotificationInfo.shouldBeSavedOnClose()); - } - - @Test - public void testAlertCallsUpdateNotificationChannel_channelImportanceMin() - throws Exception { - mNotificationChannel.setImportance(IMPORTANCE_MIN); - mNotificationInfo.bindNotification( - mMockPackageManager, - mMockINotificationManager, - mOnUserInteractionCallback, - mChannelEditorDialogController, - TEST_PACKAGE_NAME, - mNotificationChannel, - mEntry, - null, - null, - null, - mUiEventLogger, - true, - false, - false, - mAssistantFeedbackController, - mMetricsLogger, - null); - - assertEquals(mContext.getString(R.string.inline_done_button), - ((TextView) mNotificationInfo.findViewById(R.id.done)).getText()); - mNotificationInfo.findViewById(R.id.alert).performClick(); - assertEquals(mContext.getString(R.string.inline_ok_button), - ((TextView) mNotificationInfo.findViewById(R.id.done)).getText()); - mNotificationInfo.findViewById(R.id.done).performClick(); - mNotificationInfo.handleCloseControls(true, false); - - mTestableLooper.processAllMessages(); - ArgumentCaptor<NotificationChannel> updated = - ArgumentCaptor.forClass(NotificationChannel.class); - verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage( - anyString(), eq(TEST_UID), updated.capture()); - assertTrue((updated.getValue().getUserLockedFields() & USER_LOCKED_IMPORTANCE) != 0); - assertEquals(IMPORTANCE_DEFAULT, updated.getValue().getImportance()); - assertFalse(mNotificationInfo.shouldBeSavedOnClose()); - } - - @Test - public void testAdjustImportanceTemporarilyAllowsReordering() throws Exception { - mNotificationChannel.setImportance(IMPORTANCE_DEFAULT); - doStandardBind(); - - mNotificationInfo.findViewById(R.id.silence).performClick(); - mNotificationInfo.findViewById(R.id.done).performClick(); - mNotificationInfo.handleCloseControls(true, false); - - verify(mOnUserInteractionCallback).onImportanceChanged(mEntry); - assertFalse(mNotificationInfo.shouldBeSavedOnClose()); - } - - @Test - public void testDoneText() - throws Exception { - mNotificationChannel.setImportance(IMPORTANCE_LOW); - mNotificationInfo.bindNotification( - mMockPackageManager, - mMockINotificationManager, - mOnUserInteractionCallback, - mChannelEditorDialogController, - TEST_PACKAGE_NAME, - mNotificationChannel, - mEntry, - null, - null, - null, - mUiEventLogger, - true, - false, - false, - mAssistantFeedbackController, - mMetricsLogger, - null); - - assertEquals(mContext.getString(R.string.inline_done_button), - ((TextView) mNotificationInfo.findViewById(R.id.done)).getText()); - mNotificationInfo.findViewById(R.id.alert).performClick(); - assertEquals(mContext.getString(R.string.inline_ok_button), - ((TextView) mNotificationInfo.findViewById(R.id.done)).getText()); - mNotificationInfo.findViewById(R.id.silence).performClick(); - assertEquals(mContext.getString(R.string.inline_done_button), - ((TextView) mNotificationInfo.findViewById(R.id.done)).getText()); - } - - @Test - public void testUnSilenceCallsUpdateNotificationChannel_channelImportanceUnspecified() - throws Exception { - mNotificationChannel.setImportance(IMPORTANCE_LOW); - mNotificationInfo.bindNotification( - mMockPackageManager, - mMockINotificationManager, - mOnUserInteractionCallback, - mChannelEditorDialogController, - TEST_PACKAGE_NAME, - mNotificationChannel, - mEntry, - null, - null, - null, - mUiEventLogger, - true, - false, - false, - mAssistantFeedbackController, - mMetricsLogger, - null); - - mNotificationInfo.findViewById(R.id.alert).performClick(); - mNotificationInfo.findViewById(R.id.done).performClick(); - mNotificationInfo.handleCloseControls(true, false); - - mTestableLooper.processAllMessages(); - ArgumentCaptor<NotificationChannel> updated = - ArgumentCaptor.forClass(NotificationChannel.class); - verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage( - anyString(), eq(TEST_UID), updated.capture()); - assertTrue((updated.getValue().getUserLockedFields() - & USER_LOCKED_IMPORTANCE) != 0); - assertEquals(IMPORTANCE_DEFAULT, updated.getValue().getImportance()); - assertFalse(mNotificationInfo.shouldBeSavedOnClose()); - } - - @Test - public void testCloseControlsDoesNotUpdateIfSaveIsFalse() throws Exception { - mNotificationChannel.setImportance(IMPORTANCE_LOW); - mNotificationInfo.bindNotification( - mMockPackageManager, - mMockINotificationManager, - mOnUserInteractionCallback, - mChannelEditorDialogController, - TEST_PACKAGE_NAME, - mNotificationChannel, - mEntry, - null, - null, - null, - mUiEventLogger, - true, - false, - false, - mAssistantFeedbackController, - mMetricsLogger, - null); - - mNotificationInfo.findViewById(R.id.alert).performClick(); - mNotificationInfo.findViewById(R.id.done).performClick(); - mNotificationInfo.handleCloseControls(false, false); - - mTestableLooper.processAllMessages(); - verify(mMockINotificationManager, never()).updateNotificationChannelForPackage( - eq(TEST_PACKAGE_NAME), eq(TEST_UID), eq(mNotificationChannel)); - - assertEquals(1, mUiEventLogger.numLogs()); - assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.getId(), - mUiEventLogger.eventId(0)); - } - - @Test - public void testCloseControlsUpdatesWhenCheckSaveListenerUsesCallback() throws Exception { - mNotificationChannel.setImportance(IMPORTANCE_LOW); - mNotificationInfo.bindNotification( - mMockPackageManager, - mMockINotificationManager, - mOnUserInteractionCallback, - mChannelEditorDialogController, - TEST_PACKAGE_NAME, - mNotificationChannel, - mEntry, - null, - null, - null, - mUiEventLogger, - true, - false, - false, - mAssistantFeedbackController, - mMetricsLogger, - null); - - mNotificationInfo.findViewById(R.id.alert).performClick(); - mNotificationInfo.findViewById(R.id.done).performClick(); - mTestableLooper.processAllMessages(); - verify(mMockINotificationManager, never()).updateNotificationChannelForPackage( - eq(TEST_PACKAGE_NAME), eq(TEST_UID), eq(mNotificationChannel)); - - mNotificationInfo.handleCloseControls(true, false); - - mTestableLooper.processAllMessages(); - verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage( - eq(TEST_PACKAGE_NAME), eq(TEST_UID), eq(mNotificationChannel)); - } - - @Test - public void testCloseControls_withoutHittingApply() throws Exception { - mNotificationChannel.setImportance(IMPORTANCE_LOW); - mNotificationInfo.bindNotification( - mMockPackageManager, - mMockINotificationManager, - mOnUserInteractionCallback, - mChannelEditorDialogController, - TEST_PACKAGE_NAME, - mNotificationChannel, - mEntry, - null, - null, - null, - mUiEventLogger, - true, - false, - false, - mAssistantFeedbackController, - mMetricsLogger, - null); - - mNotificationInfo.findViewById(R.id.alert).performClick(); - - assertFalse(mNotificationInfo.shouldBeSavedOnClose()); - } - - @Test - public void testWillBeRemovedReturnsFalse() throws Exception { - assertFalse(mNotificationInfo.willBeRemoved()); - - mNotificationInfo.bindNotification( - mMockPackageManager, - mMockINotificationManager, - mOnUserInteractionCallback, - mChannelEditorDialogController, - TEST_PACKAGE_NAME, - mNotificationChannel, - mEntry, - null, - null, - null, - mUiEventLogger, - true, - false, - false, - mAssistantFeedbackController, - mMetricsLogger, - null); - - assertFalse(mNotificationInfo.willBeRemoved()); - } - - - @Test - @DisableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI) - public void testBindNotification_HidesFeedbackLink_flagOff() throws Exception { - doStandardBind(); - assertEquals(GONE, mNotificationInfo.findViewById(R.id.feedback).getVisibility()); - } - - @Test - @EnableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI) - public void testBindNotification_SetsFeedbackLink_isReservedChannel() throws RemoteException { - mEntry.setRanking(new RankingBuilder(mEntry.getRanking()) - .setSummarization("something").build()); - final CountDownLatch latch = new CountDownLatch(1); - mNotificationInfo.bindNotification( - mMockPackageManager, - mMockINotificationManager, - mOnUserInteractionCallback, - mChannelEditorDialogController, - TEST_PACKAGE_NAME, - mClassifiedNotificationChannel, - mEntry, - null, - null, - (View v, Intent intent) -> { - latch.countDown(); - }, - mUiEventLogger, - true, - false, - false, - mAssistantFeedbackController, - mMetricsLogger, - null); - - final View feedback = mNotificationInfo.findViewById(R.id.feedback); - assertEquals(VISIBLE, feedback.getVisibility()); - feedback.performClick(); - // Verify that listener was triggered. - assertEquals(0, latch.getCount()); - } - - @Test - @EnableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI) - public void testBindNotification_hidesFeedbackLink_notReservedChannel() throws Exception { - doStandardBind(); - - assertEquals(GONE, mNotificationInfo.findViewById(R.id.feedback).getVisibility()); - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt new file mode 100644 index 000000000000..2945fa98caad --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt @@ -0,0 +1,910 @@ +/* + * 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.statusbar.notification.row + +import android.app.Flags +import android.app.INotificationManager +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationChannel.SOCIAL_MEDIA_ID +import android.app.NotificationChannelGroup +import android.app.NotificationManager +import android.app.NotificationManager.IMPORTANCE_LOW +import android.app.PendingIntent +import android.app.Person +import android.content.ComponentName +import android.content.Intent +import android.content.mockPackageManager +import android.content.pm.ActivityInfo +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.graphics.drawable.Drawable +import android.os.RemoteException +import android.os.UserHandle +import android.os.testableLooper +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.print.PrintManager +import android.service.notification.StatusBarNotification +import android.telecom.TelecomManager +import android.testing.TestableLooper.RunWithLooper +import android.view.LayoutInflater +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import android.widget.ImageView +import android.widget.TextView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.logging.MetricsLogger +import com.android.internal.logging.UiEventLogger +import com.android.internal.logging.metricsLogger +import com.android.internal.logging.uiEventLoggerFake +import com.android.systemui.Dependency +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testCase +import com.android.systemui.res.R +import com.android.systemui.statusbar.RankingBuilder +import com.android.systemui.statusbar.notification.AssistantFeedbackController +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.telecom.telecomManager +import com.google.common.truth.Truth.assertThat +import java.util.concurrent.CountDownLatch +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +@RunWithLooper +class NotificationInfoTest : SysuiTestCase() { + private val kosmos = Kosmos().also { it.testCase = this } + + private lateinit var underTest: NotificationInfo + private lateinit var notificationChannel: NotificationChannel + private lateinit var defaultNotificationChannel: NotificationChannel + private lateinit var classifiedNotificationChannel: NotificationChannel + private lateinit var sbn: StatusBarNotification + private lateinit var entry: NotificationEntry + + private val mockPackageManager = kosmos.mockPackageManager + private val uiEventLogger = kosmos.uiEventLoggerFake + private val testableLooper by lazy { kosmos.testableLooper } + + private val onUserInteractionCallback = mock<OnUserInteractionCallback>() + private val mockINotificationManager = mock<INotificationManager>() + private val channelEditorDialogController = mock<ChannelEditorDialogController>() + private val assistantFeedbackController = mock<AssistantFeedbackController>() + + @Before + fun setUp() { + mContext.addMockSystemService(TelecomManager::class.java, kosmos.telecomManager) + + mDependency.injectTestDependency(Dependency.BG_LOOPER, testableLooper.looper) + + // Inflate the layout + val inflater = LayoutInflater.from(mContext) + underTest = inflater.inflate(R.layout.notification_info, null) as NotificationInfo + + underTest.setGutsParent(mock<NotificationGuts>()) + + // Our view is never attached to a window so the View#post methods in NotificationInfo never + // get called. Setting this will skip the post and do the action immediately. + underTest.mSkipPost = true + + // PackageManager must return a packageInfo and applicationInfo. + val packageInfo = PackageInfo() + packageInfo.packageName = TEST_PACKAGE_NAME + whenever(mockPackageManager.getPackageInfo(eq(TEST_PACKAGE_NAME), anyInt())) + .thenReturn(packageInfo) + val applicationInfo = ApplicationInfo() + applicationInfo.uid = TEST_UID // non-zero + val systemPackageInfo = PackageInfo() + systemPackageInfo.packageName = TEST_SYSTEM_PACKAGE_NAME + whenever(mockPackageManager.getPackageInfo(eq(TEST_SYSTEM_PACKAGE_NAME), anyInt())) + .thenReturn(systemPackageInfo) + whenever(mockPackageManager.getPackageInfo(eq("android"), anyInt())).thenReturn(packageInfo) + + val assistant = ComponentName("package", "service") + whenever(mockINotificationManager.allowedNotificationAssistant).thenReturn(assistant) + val ri = ResolveInfo() + ri.activityInfo = ActivityInfo() + ri.activityInfo.packageName = assistant.packageName + ri.activityInfo.name = "activity" + whenever(mockPackageManager.queryIntentActivities(any(), anyInt())).thenReturn(listOf(ri)) + + // Package has one channel by default. + whenever( + mockINotificationManager.getNumNotificationChannelsForPackage( + eq(TEST_PACKAGE_NAME), + eq(TEST_UID), + anyBoolean(), + ) + ) + .thenReturn(1) + + // Some test channels. + notificationChannel = NotificationChannel(TEST_CHANNEL, TEST_CHANNEL_NAME, IMPORTANCE_LOW) + defaultNotificationChannel = + NotificationChannel( + NotificationChannel.DEFAULT_CHANNEL_ID, + TEST_CHANNEL_NAME, + IMPORTANCE_LOW, + ) + classifiedNotificationChannel = + NotificationChannel(SOCIAL_MEDIA_ID, "social", IMPORTANCE_LOW) + + val notification = Notification() + notification.extras.putParcelable( + Notification.EXTRA_BUILDER_APPLICATION_INFO, + applicationInfo, + ) + sbn = + StatusBarNotification( + TEST_PACKAGE_NAME, + TEST_PACKAGE_NAME, + 0, + null, + TEST_UID, + 0, + notification, + UserHandle.getUserHandleForUid(TEST_UID), + null, + 0, + ) + entry = NotificationEntryBuilder().setSbn(sbn).build() + whenever(assistantFeedbackController.isFeedbackEnabled).thenReturn(false) + whenever(assistantFeedbackController.getInlineDescriptionResource(any())) + .thenReturn(R.string.notification_channel_summary_automatic) + } + + @Test + fun testBindNotification_SetsTextApplicationName() { + whenever(mockPackageManager.getApplicationLabel(any())).thenReturn("App Name") + bindNotification() + val textView = underTest.findViewById<TextView>(R.id.pkg_name) + assertThat(textView.text.toString()).contains("App Name") + assertThat(underTest.findViewById<View>(R.id.header).visibility).isEqualTo(VISIBLE) + } + + @Test + fun testBindNotification_SetsPackageIcon() { + val iconDrawable = mock<Drawable>() + whenever(mockPackageManager.getApplicationIcon(any<ApplicationInfo>())) + .thenReturn(iconDrawable) + bindNotification() + val iconView = underTest.findViewById<ImageView>(R.id.pkg_icon) + assertThat(iconView.drawable).isEqualTo(iconDrawable) + } + + @Test + fun testBindNotification_noDelegate() { + bindNotification() + val nameView = underTest.findViewById<TextView>(R.id.delegate_name) + assertThat(nameView.visibility).isEqualTo(GONE) + } + + @Test + fun testBindNotification_delegate() { + sbn = + StatusBarNotification( + TEST_PACKAGE_NAME, + "other", + 0, + null, + TEST_UID, + 0, + Notification(), + UserHandle.CURRENT, + null, + 0, + ) + val applicationInfo = ApplicationInfo() + applicationInfo.uid = 7 // non-zero + whenever(mockPackageManager.getApplicationInfo(eq("other"), anyInt())) + .thenReturn(applicationInfo) + whenever(mockPackageManager.getApplicationLabel(any())).thenReturn("Other") + + val entry = NotificationEntryBuilder().setSbn(sbn).build() + bindNotification(entry = entry) + val nameView = underTest.findViewById<TextView>(R.id.delegate_name) + assertThat(nameView.visibility).isEqualTo(VISIBLE) + assertThat(nameView.text.toString()).contains("Proxied") + } + + @Test + fun testBindNotification_GroupNameHiddenIfNoGroup() { + bindNotification() + val groupNameView = underTest.findViewById<TextView>(R.id.group_name) + assertThat(groupNameView.visibility).isEqualTo(GONE) + } + + @Test + fun testBindNotification_SetsGroupNameIfNonNull() { + notificationChannel.group = "test_group_id" + val notificationChannelGroup = NotificationChannelGroup("test_group_id", "Test Group Name") + whenever( + mockINotificationManager.getNotificationChannelGroupForPackage( + eq("test_group_id"), + eq(TEST_PACKAGE_NAME), + eq(TEST_UID), + ) + ) + .thenReturn(notificationChannelGroup) + bindNotification() + val groupNameView = underTest.findViewById<TextView>(R.id.group_name) + assertThat(groupNameView.visibility).isEqualTo(VISIBLE) + assertThat(groupNameView.text).isEqualTo("Test Group Name") + } + + @Test + fun testBindNotification_SetsTextChannelName() { + bindNotification() + val textView = underTest.findViewById<TextView>(R.id.channel_name) + assertThat(textView.text).isEqualTo(TEST_CHANNEL_NAME) + } + + @Test + fun testBindNotification_DefaultChannelDoesNotUseChannelName() { + bindNotification(notificationChannel = defaultNotificationChannel) + val textView = underTest.findViewById<TextView>(R.id.channel_name) + assertThat(textView.visibility).isEqualTo(GONE) + } + + @Test + fun testBindNotification_DefaultChannelUsesChannelNameIfMoreChannelsExist() { + // Package has more than one channel by default. + whenever( + mockINotificationManager.getNumNotificationChannelsForPackage( + eq(TEST_PACKAGE_NAME), + eq(TEST_UID), + anyBoolean(), + ) + ) + .thenReturn(10) + bindNotification(notificationChannel = defaultNotificationChannel) + val textView = underTest.findViewById<TextView>(R.id.channel_name) + assertThat(textView.visibility).isEqualTo(VISIBLE) + } + + @Test + fun testBindNotification_UnblockablePackageUsesChannelName() { + bindNotification(isNonblockable = true) + val textView = underTest.findViewById<TextView>(R.id.channel_name) + assertThat(textView.visibility).isEqualTo(VISIBLE) + } + + @Test + fun testBindNotification_SetsOnClickListenerForSettings() { + val latch = CountDownLatch(1) + bindNotification( + onSettingsClick = { _: View?, c: NotificationChannel?, _: Int -> + assertThat(c).isEqualTo(notificationChannel) + latch.countDown() + } + ) + + val settingsButton = underTest.findViewById<View>(R.id.info) + settingsButton.performClick() + // Verify that listener was triggered. + assertThat(latch.count).isEqualTo(0) + } + + @Test + fun testBindNotification_SettingsButtonInvisibleWhenNoClickListener() { + bindNotification() + val settingsButton = underTest.findViewById<View>(R.id.info) + assertThat(settingsButton.visibility != VISIBLE).isTrue() + } + + @Test + fun testBindNotification_SettingsButtonInvisibleWhenDeviceUnprovisioned() { + bindNotification( + onSettingsClick = { _: View?, c: NotificationChannel?, _: Int -> + assertThat(c).isEqualTo(notificationChannel) + }, + isDeviceProvisioned = false, + ) + val settingsButton = underTest.findViewById<View>(R.id.info) + assertThat(settingsButton.visibility != VISIBLE).isTrue() + } + + @Test + fun testBindNotification_SettingsButtonReappearsAfterSecondBind() { + bindNotification() + bindNotification(onSettingsClick = { _: View?, _: NotificationChannel?, _: Int -> }) + val settingsButton = underTest.findViewById<View>(R.id.info) + assertThat(settingsButton.visibility).isEqualTo(VISIBLE) + } + + @Test + fun testBindNotification_whenAppUnblockable() { + bindNotification(isNonblockable = true) + val view = underTest.findViewById<TextView>(R.id.non_configurable_text) + assertThat(view.visibility).isEqualTo(VISIBLE) + assertThat(view.text).isEqualTo(mContext.getString(R.string.notification_unblockable_desc)) + assertThat(underTest.findViewById<View>(R.id.interruptiveness_settings).visibility) + .isEqualTo(GONE) + } + + @Test + fun testBindNotification_whenCurrentlyInCall() { + whenever(mockINotificationManager.isInCall(anyString(), anyInt())).thenReturn(true) + + val person = Person.Builder().setName("caller").build() + val nb = + Notification.Builder(mContext, notificationChannel.id) + .setContentTitle("foo") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setStyle(Notification.CallStyle.forOngoingCall(person, mock<PendingIntent>())) + .setFullScreenIntent(mock<PendingIntent>(), true) + .addAction(Notification.Action.Builder(null, "test", null).build()) + + sbn = + StatusBarNotification( + TEST_PACKAGE_NAME, + TEST_PACKAGE_NAME, + 0, + null, + TEST_UID, + 0, + nb.build(), + UserHandle.getUserHandleForUid(TEST_UID), + null, + 0, + ) + entry.sbn = sbn + bindNotification() + val view = underTest.findViewById<TextView>(R.id.non_configurable_call_text) + assertThat(view.visibility).isEqualTo(VISIBLE) + assertThat(view.text) + .isEqualTo(mContext.getString(R.string.notification_unblockable_call_desc)) + assertThat(underTest.findViewById<View>(R.id.interruptiveness_settings).visibility) + .isEqualTo(GONE) + assertThat(underTest.findViewById<View>(R.id.non_configurable_text).visibility) + .isEqualTo(GONE) + } + + @Test + fun testBindNotification_whenCurrentlyInCall_notCall() { + whenever(mockINotificationManager.isInCall(anyString(), anyInt())).thenReturn(true) + + val nb = + Notification.Builder(mContext, notificationChannel.id) + .setContentTitle("foo") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setFullScreenIntent(mock<PendingIntent>(), true) + .addAction(Notification.Action.Builder(null, "test", null).build()) + + sbn = + StatusBarNotification( + TEST_PACKAGE_NAME, + TEST_PACKAGE_NAME, + 0, + null, + TEST_UID, + 0, + nb.build(), + UserHandle.getUserHandleForUid(TEST_UID), + null, + 0, + ) + entry.sbn = sbn + bindNotification() + assertThat(underTest.findViewById<View>(R.id.non_configurable_call_text).visibility) + .isEqualTo(GONE) + assertThat(underTest.findViewById<View>(R.id.interruptiveness_settings).visibility) + .isEqualTo(VISIBLE) + assertThat(underTest.findViewById<View>(R.id.non_configurable_text).visibility) + .isEqualTo(GONE) + } + + @Test + fun testBindNotification_automaticIsVisible() { + whenever(assistantFeedbackController.isFeedbackEnabled).thenReturn(true) + bindNotification() + assertThat(underTest.findViewById<View>(R.id.automatic).visibility).isEqualTo(VISIBLE) + assertThat(underTest.findViewById<View>(R.id.automatic_summary).visibility) + .isEqualTo(VISIBLE) + } + + @Test + fun testBindNotification_automaticIsGone() { + bindNotification() + assertThat(underTest.findViewById<View>(R.id.automatic).visibility).isEqualTo(GONE) + assertThat(underTest.findViewById<View>(R.id.automatic_summary).visibility).isEqualTo(GONE) + } + + @Test + fun testBindNotification_automaticIsSelected() { + whenever(assistantFeedbackController.isFeedbackEnabled).thenReturn(true) + notificationChannel.unlockFields(NotificationChannel.USER_LOCKED_IMPORTANCE) + bindNotification() + assertThat(underTest.findViewById<View>(R.id.automatic).isSelected).isTrue() + } + + @Test + fun testBindNotification_alertIsSelected() { + bindNotification() + assertThat(underTest.findViewById<View>(R.id.alert).isSelected).isTrue() + } + + @Test + fun testBindNotification_silenceIsSelected() { + bindNotification(wasShownHighPriority = false) + assertThat(underTest.findViewById<View>(R.id.silence).isSelected).isTrue() + } + + @Test + fun testBindNotification_DoesNotUpdateNotificationChannel() { + bindNotification() + testableLooper.processAllMessages() + verify(mockINotificationManager, never()) + .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), any()) + } + + @Test + fun testBindNotification_LogsOpen() { + bindNotification() + assertThat(uiEventLogger.numLogs()).isEqualTo(1) + assertThat(uiEventLogger.eventId(0)) + .isEqualTo(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.id) + } + + @Test + fun testDoesNotUpdateNotificationChannelAfterImportanceChanged() { + notificationChannel.importance = IMPORTANCE_LOW + bindNotification(wasShownHighPriority = false) + + underTest.findViewById<View>(R.id.alert).performClick() + testableLooper.processAllMessages() + verify(mockINotificationManager, never()) + .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), any()) + } + + @Test + fun testDoesNotUpdateNotificationChannelAfterImportanceChangedSilenced() { + notificationChannel.importance = NotificationManager.IMPORTANCE_DEFAULT + bindNotification() + + underTest.findViewById<View>(R.id.silence).performClick() + testableLooper.processAllMessages() + verify(mockINotificationManager, never()) + .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), any()) + } + + @Test + fun testDoesNotUpdateNotificationChannelAfterImportanceChangedAutomatic() { + notificationChannel.importance = NotificationManager.IMPORTANCE_DEFAULT + bindNotification() + + underTest.findViewById<View>(R.id.automatic).performClick() + testableLooper.processAllMessages() + verify(mockINotificationManager, never()) + .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), any()) + } + + @Test + fun testHandleCloseControls_persistAutomatic() { + whenever(assistantFeedbackController.isFeedbackEnabled).thenReturn(true) + notificationChannel.unlockFields(NotificationChannel.USER_LOCKED_IMPORTANCE) + bindNotification() + + underTest.handleCloseControls(true, false) + testableLooper.processAllMessages() + verify(mockINotificationManager).unlockNotificationChannel(anyString(), eq(TEST_UID), any()) + } + + @Test + fun testHandleCloseControls_DoesNotUpdateNotificationChannelIfUnchanged() { + val originalImportance = notificationChannel.importance + bindNotification() + + underTest.handleCloseControls(true, false) + testableLooper.processAllMessages() + verify(mockINotificationManager) + .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), any()) + assertThat(notificationChannel.importance).isEqualTo(originalImportance) + + assertThat(uiEventLogger.numLogs()).isEqualTo(2) + assertThat(uiEventLogger.eventId(0)) + .isEqualTo(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.id) + // The SAVE_IMPORTANCE event is logged whenever importance is saved, even if unchanged. + assertThat(uiEventLogger.eventId(1)) + .isEqualTo(NotificationControlsEvent.NOTIFICATION_CONTROLS_SAVE_IMPORTANCE.id) + } + + @Test + fun testHandleCloseControls_DoesNotUpdateNotificationChannelIfUnspecified() { + notificationChannel.importance = NotificationManager.IMPORTANCE_UNSPECIFIED + bindNotification() + + underTest.handleCloseControls(true, false) + + testableLooper.processAllMessages() + verify(mockINotificationManager) + .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), any()) + assertThat(notificationChannel.importance) + .isEqualTo(NotificationManager.IMPORTANCE_UNSPECIFIED) + } + + @Test + fun testSilenceCallsUpdateNotificationChannel() { + notificationChannel.importance = NotificationManager.IMPORTANCE_DEFAULT + bindNotification() + + underTest.findViewById<View>(R.id.silence).performClick() + underTest.findViewById<View>(R.id.done).performClick() + underTest.handleCloseControls(true, false) + + testableLooper.processAllMessages() + val updated = argumentCaptor<NotificationChannel>() + verify(mockINotificationManager) + .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), updated.capture()) + assertThat( + updated.firstValue.userLockedFields and NotificationChannel.USER_LOCKED_IMPORTANCE + ) + .isNotEqualTo(0) + assertThat(updated.firstValue.importance).isEqualTo(IMPORTANCE_LOW) + + assertThat(uiEventLogger.numLogs()).isEqualTo(2) + assertThat(uiEventLogger.eventId(0)) + .isEqualTo(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.id) + assertThat(uiEventLogger.eventId(1)) + .isEqualTo(NotificationControlsEvent.NOTIFICATION_CONTROLS_SAVE_IMPORTANCE.id) + assertThat(underTest.shouldBeSavedOnClose()).isFalse() + } + + @Test + fun testUnSilenceCallsUpdateNotificationChannel() { + notificationChannel.importance = IMPORTANCE_LOW + bindNotification(wasShownHighPriority = false) + + underTest.findViewById<View>(R.id.alert).performClick() + underTest.findViewById<View>(R.id.done).performClick() + underTest.handleCloseControls(true, false) + + testableLooper.processAllMessages() + val updated = argumentCaptor<NotificationChannel>() + verify(mockINotificationManager) + .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), updated.capture()) + assertThat( + updated.firstValue.userLockedFields and NotificationChannel.USER_LOCKED_IMPORTANCE + ) + .isNotEqualTo(0) + assertThat(updated.firstValue.importance).isEqualTo(NotificationManager.IMPORTANCE_DEFAULT) + assertThat(underTest.shouldBeSavedOnClose()).isFalse() + } + + @Test + fun testAutomaticUnlocksUserImportance() { + whenever(assistantFeedbackController.isFeedbackEnabled).thenReturn(true) + notificationChannel.importance = NotificationManager.IMPORTANCE_DEFAULT + notificationChannel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE) + bindNotification() + + underTest.findViewById<View>(R.id.automatic).performClick() + underTest.findViewById<View>(R.id.done).performClick() + underTest.handleCloseControls(true, false) + + testableLooper.processAllMessages() + verify(mockINotificationManager).unlockNotificationChannel(anyString(), eq(TEST_UID), any()) + assertThat(notificationChannel.importance).isEqualTo(NotificationManager.IMPORTANCE_DEFAULT) + assertThat(underTest.shouldBeSavedOnClose()).isFalse() + } + + @Test + fun testSilenceCallsUpdateNotificationChannel_channelImportanceUnspecified() { + notificationChannel.importance = NotificationManager.IMPORTANCE_UNSPECIFIED + bindNotification() + + underTest.findViewById<View>(R.id.silence).performClick() + underTest.findViewById<View>(R.id.done).performClick() + underTest.handleCloseControls(true, false) + + testableLooper.processAllMessages() + val updated = argumentCaptor<NotificationChannel>() + verify(mockINotificationManager) + .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), updated.capture()) + assertThat( + updated.firstValue.userLockedFields and NotificationChannel.USER_LOCKED_IMPORTANCE + ) + .isNotEqualTo(0) + assertThat(updated.firstValue.importance).isEqualTo(IMPORTANCE_LOW) + assertThat(underTest.shouldBeSavedOnClose()).isFalse() + } + + @Test + fun testSilenceCallsUpdateNotificationChannel_channelImportanceMin() { + notificationChannel.importance = NotificationManager.IMPORTANCE_MIN + bindNotification(wasShownHighPriority = false) + + assertThat((underTest.findViewById<View>(R.id.done) as TextView).text) + .isEqualTo(mContext.getString(R.string.inline_done_button)) + underTest.findViewById<View>(R.id.silence).performClick() + assertThat((underTest.findViewById<View>(R.id.done) as TextView).text) + .isEqualTo(mContext.getString(R.string.inline_done_button)) + underTest.findViewById<View>(R.id.done).performClick() + underTest.handleCloseControls(true, false) + + testableLooper.processAllMessages() + val updated = argumentCaptor<NotificationChannel>() + verify(mockINotificationManager) + .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), updated.capture()) + assertThat( + updated.firstValue.userLockedFields and NotificationChannel.USER_LOCKED_IMPORTANCE + ) + .isNotEqualTo(0) + assertThat(updated.firstValue.importance).isEqualTo(NotificationManager.IMPORTANCE_MIN) + assertThat(underTest.shouldBeSavedOnClose()).isFalse() + } + + @Test + @Throws(RemoteException::class) + fun testSilence_closeGutsThenTryToSave() { + notificationChannel.importance = NotificationManager.IMPORTANCE_DEFAULT + bindNotification(wasShownHighPriority = false) + + underTest.findViewById<View>(R.id.silence).performClick() + underTest.handleCloseControls(false, false) + underTest.handleCloseControls(true, false) + + testableLooper.processAllMessages() + + assertThat(notificationChannel.importance).isEqualTo(NotificationManager.IMPORTANCE_DEFAULT) + assertThat(underTest.shouldBeSavedOnClose()).isFalse() + } + + @Test + fun testAlertCallsUpdateNotificationChannel_channelImportanceMin() { + notificationChannel.importance = NotificationManager.IMPORTANCE_MIN + bindNotification(wasShownHighPriority = false) + + assertThat((underTest.findViewById<View>(R.id.done) as TextView).text) + .isEqualTo(mContext.getString(R.string.inline_done_button)) + underTest.findViewById<View>(R.id.alert).performClick() + assertThat((underTest.findViewById<View>(R.id.done) as TextView).text) + .isEqualTo(mContext.getString(R.string.inline_ok_button)) + underTest.findViewById<View>(R.id.done).performClick() + underTest.handleCloseControls(true, false) + + testableLooper.processAllMessages() + val updated = argumentCaptor<NotificationChannel>() + verify(mockINotificationManager) + .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), updated.capture()) + assertThat( + updated.firstValue.userLockedFields and NotificationChannel.USER_LOCKED_IMPORTANCE + ) + .isNotEqualTo(0) + assertThat(updated.firstValue.importance).isEqualTo(NotificationManager.IMPORTANCE_DEFAULT) + assertThat(underTest.shouldBeSavedOnClose()).isFalse() + } + + @Test + fun testAdjustImportanceTemporarilyAllowsReordering() { + notificationChannel.importance = NotificationManager.IMPORTANCE_DEFAULT + bindNotification() + + underTest.findViewById<View>(R.id.silence).performClick() + underTest.findViewById<View>(R.id.done).performClick() + underTest.handleCloseControls(true, false) + + verify(onUserInteractionCallback).onImportanceChanged(entry) + assertThat(underTest.shouldBeSavedOnClose()).isFalse() + } + + @Test + fun testDoneText() { + notificationChannel.importance = IMPORTANCE_LOW + bindNotification(wasShownHighPriority = false) + + assertThat((underTest.findViewById<View>(R.id.done) as TextView).text) + .isEqualTo(mContext.getString(R.string.inline_done_button)) + underTest.findViewById<View>(R.id.alert).performClick() + assertThat((underTest.findViewById<View>(R.id.done) as TextView).text) + .isEqualTo(mContext.getString(R.string.inline_ok_button)) + underTest.findViewById<View>(R.id.silence).performClick() + assertThat((underTest.findViewById<View>(R.id.done) as TextView).text) + .isEqualTo(mContext.getString(R.string.inline_done_button)) + } + + @Test + fun testUnSilenceCallsUpdateNotificationChannel_channelImportanceUnspecified() { + notificationChannel.importance = IMPORTANCE_LOW + bindNotification(wasShownHighPriority = false) + + underTest.findViewById<View>(R.id.alert).performClick() + underTest.findViewById<View>(R.id.done).performClick() + underTest.handleCloseControls(true, false) + + testableLooper.processAllMessages() + val updated = argumentCaptor<NotificationChannel>() + verify(mockINotificationManager) + .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), updated.capture()) + assertThat( + updated.firstValue.userLockedFields and NotificationChannel.USER_LOCKED_IMPORTANCE + ) + .isNotEqualTo(0) + assertThat(updated.firstValue.importance).isEqualTo(NotificationManager.IMPORTANCE_DEFAULT) + assertThat(underTest.shouldBeSavedOnClose()).isFalse() + } + + @Test + fun testCloseControlsDoesNotUpdateIfSaveIsFalse() { + notificationChannel.importance = IMPORTANCE_LOW + bindNotification(wasShownHighPriority = false) + + underTest.findViewById<View>(R.id.alert).performClick() + underTest.findViewById<View>(R.id.done).performClick() + underTest.handleCloseControls(false, false) + + testableLooper.processAllMessages() + verify(mockINotificationManager, never()) + .updateNotificationChannelForPackage( + eq(TEST_PACKAGE_NAME), + eq(TEST_UID), + eq(notificationChannel), + ) + + assertThat(uiEventLogger.numLogs()).isEqualTo(1) + assertThat(uiEventLogger.eventId(0)) + .isEqualTo(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.id) + } + + @Test + fun testCloseControlsUpdatesWhenCheckSaveListenerUsesCallback() { + notificationChannel.importance = IMPORTANCE_LOW + bindNotification(wasShownHighPriority = false) + + underTest.findViewById<View>(R.id.alert).performClick() + underTest.findViewById<View>(R.id.done).performClick() + testableLooper.processAllMessages() + verify(mockINotificationManager, never()) + .updateNotificationChannelForPackage( + eq(TEST_PACKAGE_NAME), + eq(TEST_UID), + eq(notificationChannel), + ) + + underTest.handleCloseControls(true, false) + + testableLooper.processAllMessages() + verify(mockINotificationManager) + .updateNotificationChannelForPackage( + eq(TEST_PACKAGE_NAME), + eq(TEST_UID), + eq(notificationChannel), + ) + } + + @Test + fun testCloseControls_withoutHittingApply() { + notificationChannel.importance = IMPORTANCE_LOW + bindNotification(wasShownHighPriority = false) + + underTest.findViewById<View>(R.id.alert).performClick() + + assertThat(underTest.shouldBeSavedOnClose()).isFalse() + } + + @Test + fun testWillBeRemovedReturnsFalse() { + assertThat(underTest.willBeRemoved()).isFalse() + + bindNotification(wasShownHighPriority = false) + + assertThat(underTest.willBeRemoved()).isFalse() + } + + @Test + @DisableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI) + @Throws(Exception::class) + fun testBindNotification_HidesFeedbackLink_flagOff() { + bindNotification() + assertThat(underTest.findViewById<View>(R.id.feedback).visibility).isEqualTo(GONE) + } + + @Test + @EnableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI) + @Throws(RemoteException::class) + fun testBindNotification_SetsFeedbackLink_isReservedChannel() { + entry.setRanking(RankingBuilder(entry.ranking).setSummarization("something").build()) + val latch = CountDownLatch(1) + bindNotification( + notificationChannel = classifiedNotificationChannel, + onFeedbackClickListener = { _: View?, _: Intent? -> latch.countDown() }, + wasShownHighPriority = false, + ) + + val feedback: View = underTest.findViewById(R.id.feedback) + assertThat(feedback.visibility).isEqualTo(VISIBLE) + feedback.performClick() + // Verify that listener was triggered. + assertThat(latch.count).isEqualTo(0) + } + + @Test + @EnableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI) + @Throws(Exception::class) + fun testBindNotification_hidesFeedbackLink_notReservedChannel() { + bindNotification() + + assertThat(underTest.findViewById<View>(R.id.feedback).visibility).isEqualTo(GONE) + } + + private fun bindNotification( + pm: PackageManager = this.mockPackageManager, + iNotificationManager: INotificationManager = this.mockINotificationManager, + onUserInteractionCallback: OnUserInteractionCallback = this.onUserInteractionCallback, + channelEditorDialogController: ChannelEditorDialogController = + this.channelEditorDialogController, + pkg: String = TEST_PACKAGE_NAME, + notificationChannel: NotificationChannel = this.notificationChannel, + entry: NotificationEntry = this.entry, + onSettingsClick: NotificationInfo.OnSettingsClickListener? = null, + onAppSettingsClick: NotificationInfo.OnAppSettingsClickListener? = null, + onFeedbackClickListener: NotificationInfo.OnFeedbackClickListener? = null, + uiEventLogger: UiEventLogger = this.uiEventLogger, + isDeviceProvisioned: Boolean = true, + isNonblockable: Boolean = false, + wasShownHighPriority: Boolean = true, + assistantFeedbackController: AssistantFeedbackController = this.assistantFeedbackController, + metricsLogger: MetricsLogger = kosmos.metricsLogger, + onCloseClick: View.OnClickListener? = null, + ) { + underTest.bindNotification( + pm, + iNotificationManager, + onUserInteractionCallback, + channelEditorDialogController, + pkg, + notificationChannel, + entry, + onSettingsClick, + onAppSettingsClick, + onFeedbackClickListener, + uiEventLogger, + isDeviceProvisioned, + isNonblockable, + wasShownHighPriority, + assistantFeedbackController, + metricsLogger, + onCloseClick, + ) + } + + companion object { + private const val TEST_PACKAGE_NAME = "test_package" + private const val TEST_SYSTEM_PACKAGE_NAME = PrintManager.PRINT_SPOOLER_PACKAGE_NAME + private const val TEST_UID = 1 + private const val TEST_CHANNEL = "test_channel" + private const val TEST_CHANNEL_NAME = "TEST CHANNEL NAME" + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorTest.kt index 8eea2a8e6121..048028cdc0fa 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorTest.kt @@ -21,52 +21,44 @@ 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.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository -import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.plugins.statusbar.StatusBarStateController -import com.android.systemui.power.data.repository.FakePowerRepository -import com.android.systemui.power.domain.interactor.PowerInteractorFactory -import com.android.systemui.statusbar.LockscreenShadeTransitionController -import com.android.systemui.statusbar.phone.ScreenOffAnimationController -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testCase +import com.android.systemui.plugins.statusbar.statusBarStateController +import com.android.systemui.power.data.repository.fakePowerRepository +import com.android.systemui.statusbar.lockscreenShadeTransitionController +import com.android.systemui.statusbar.phone.screenOffAnimationController import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.eq import org.mockito.Mockito.isNull import org.mockito.Mockito.verify +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) @SmallTest class NotificationShelfInteractorTest : SysuiTestCase() { - private val keyguardRepository = FakeKeyguardRepository() - private val deviceEntryFaceAuthRepository = FakeDeviceEntryFaceAuthRepository() - - private val screenOffAnimationController = - mock<ScreenOffAnimationController>().also { - whenever(it.allowWakeUpIfDozing()).thenReturn(true) + private val kosmos = + Kosmos().apply { + testCase = this@NotificationShelfInteractorTest + lockscreenShadeTransitionController = mock() + screenOffAnimationController = mock() + statusBarStateController = mock() + whenever(screenOffAnimationController.allowWakeUpIfDozing()).thenReturn(true) } - private val statusBarStateController: StatusBarStateController = mock() - private val powerRepository = FakePowerRepository() - private val powerInteractor = - PowerInteractorFactory.create( - repository = powerRepository, - screenOffAnimationController = screenOffAnimationController, - statusBarStateController = statusBarStateController, - ) - .powerInteractor - - private val keyguardTransitionController: LockscreenShadeTransitionController = mock() - private val underTest = - NotificationShelfInteractor( - keyguardRepository, - deviceEntryFaceAuthRepository, - powerInteractor, - keyguardTransitionController, - ) + private val underTest = kosmos.notificationShelfInteractor + + private val keyguardRepository = kosmos.fakeKeyguardRepository + private val deviceEntryFaceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository + + private val statusBarStateController = kosmos.statusBarStateController + private val powerRepository = kosmos.fakePowerRepository + private val keyguardTransitionController = kosmos.lockscreenShadeTransitionController @Test fun shelfIsNotStatic_whenKeyguardNotShowing() = runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt index e2fb3ba11a02..d570f18e35d8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt @@ -19,73 +19,53 @@ package com.android.systemui.statusbar.notification.shelf.ui.viewmodel import android.os.PowerManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.SysUITestComponent -import com.android.systemui.SysUITestModule import com.android.systemui.SysuiTestCase -import com.android.systemui.TestMocksModule -import com.android.systemui.collectLastValue -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository -import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.power.data.repository.FakePowerRepository -import com.android.systemui.runTest -import com.android.systemui.statusbar.LockscreenShadeTransitionController -import com.android.systemui.statusbar.SysuiStatusBarStateController -import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModelModule -import com.android.systemui.statusbar.phone.ScreenOffAnimationController -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testCase +import com.android.systemui.plugins.statusbar.statusBarStateController +import com.android.systemui.power.data.repository.fakePowerRepository +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.statusbar.lockscreenShadeTransitionController +import com.android.systemui.statusbar.phone.screenOffAnimationController import com.google.common.truth.Truth.assertThat -import dagger.BindsInstance -import dagger.Component +import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.eq import org.mockito.Mockito import org.mockito.Mockito.verify +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) @SmallTest class NotificationShelfViewModelTest : SysuiTestCase() { - @Component(modules = [SysUITestModule::class, ActivatableNotificationViewModelModule::class]) - @SysUISingleton - interface TestComponent : SysUITestComponent<NotificationShelfViewModel> { - - val deviceEntryFaceAuthRepository: FakeDeviceEntryFaceAuthRepository - val keyguardRepository: FakeKeyguardRepository - val powerRepository: FakePowerRepository - - @Component.Factory - interface Factory { - fun create( - @BindsInstance test: SysuiTestCase, - mocks: TestMocksModule, - ): TestComponent + private val kosmos = + Kosmos().apply { + testCase = this@NotificationShelfViewModelTest + lockscreenShadeTransitionController = mock() + screenOffAnimationController = mock() + statusBarStateController = mock() + whenever(screenOffAnimationController.allowWakeUpIfDozing()).thenReturn(true) } - } - - private val keyguardTransitionController: LockscreenShadeTransitionController = mock() - private val screenOffAnimationController: ScreenOffAnimationController = mock { - whenever(allowWakeUpIfDozing()).thenReturn(true) - } - private val statusBarStateController: SysuiStatusBarStateController = mock() - - private val testComponent: TestComponent = - DaggerNotificationShelfViewModelTest_TestComponent.factory() - .create( - test = this, - mocks = - TestMocksModule( - lockscreenShadeTransitionController = keyguardTransitionController, - screenOffAnimationController = screenOffAnimationController, - statusBarStateController = statusBarStateController, - ) - ) + private val underTest = kosmos.notificationShelfViewModel + private val deviceEntryFaceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository + private val keyguardRepository = kosmos.fakeKeyguardRepository + private val keyguardTransitionController = kosmos.lockscreenShadeTransitionController + private val powerRepository = kosmos.fakePowerRepository @Test fun canModifyColorOfNotifications_whenKeyguardNotShowing() = - testComponent.runTest { + kosmos.runTest { val canModifyNotifColor by collectLastValue(underTest.canModifyColorOfNotifications) keyguardRepository.setKeyguardShowing(false) @@ -95,7 +75,7 @@ class NotificationShelfViewModelTest : SysuiTestCase() { @Test fun canModifyColorOfNotifications_whenKeyguardShowingAndNotBypass() = - testComponent.runTest { + kosmos.runTest { val canModifyNotifColor by collectLastValue(underTest.canModifyColorOfNotifications) keyguardRepository.setKeyguardShowing(true) @@ -106,7 +86,7 @@ class NotificationShelfViewModelTest : SysuiTestCase() { @Test fun cannotModifyColorOfNotifications_whenBypass() = - testComponent.runTest { + kosmos.runTest { val canModifyNotifColor by collectLastValue(underTest.canModifyColorOfNotifications) keyguardRepository.setKeyguardShowing(true) @@ -117,7 +97,7 @@ class NotificationShelfViewModelTest : SysuiTestCase() { @Test fun isClickable_whenKeyguardShowing() = - testComponent.runTest { + kosmos.runTest { val isClickable by collectLastValue(underTest.isClickable) keyguardRepository.setKeyguardShowing(true) @@ -127,7 +107,7 @@ class NotificationShelfViewModelTest : SysuiTestCase() { @Test fun isNotClickable_whenKeyguardNotShowing() = - testComponent.runTest { + kosmos.runTest { val isClickable by collectLastValue(underTest.isClickable) keyguardRepository.setKeyguardShowing(false) @@ -137,7 +117,7 @@ class NotificationShelfViewModelTest : SysuiTestCase() { @Test fun onClicked_goesToLockedShade() = - with(testComponent) { + kosmos.runTest { whenever(statusBarStateController.isDozing).thenReturn(true) underTest.onShelfClicked() @@ -146,4 +126,48 @@ class NotificationShelfViewModelTest : SysuiTestCase() { assertThat(powerRepository.lastWakeReason).isEqualTo(PowerManager.WAKE_REASON_GESTURE) verify(keyguardTransitionController).goToLockedShade(Mockito.isNull(), eq(true)) } + + @Test + @EnableSceneContainer + fun isAlignedToEnd_splitShade_true() = + kosmos.runTest { + val isShelfAlignedToEnd by collectLastValue(underTest.isAlignedToEnd) + + kosmos.enableSplitShade() + + assertThat(isShelfAlignedToEnd).isTrue() + } + + @Test + @EnableSceneContainer + fun isAlignedToEnd_singleShade_false() = + kosmos.runTest { + val isShelfAlignedToEnd by collectLastValue(underTest.isAlignedToEnd) + + kosmos.enableSingleShade() + + assertThat(isShelfAlignedToEnd).isFalse() + } + + @Test + @EnableSceneContainer + fun isAlignedToEnd_dualShade_wideScreen_false() = + kosmos.runTest { + val isShelfAlignedToEnd by collectLastValue(underTest.isAlignedToEnd) + + kosmos.enableDualShade(wideLayout = true) + + assertThat(isShelfAlignedToEnd).isFalse() + } + + @Test + @EnableSceneContainer + fun isAlignedToEnd_dualShade_narrowScreen_false() = + kosmos.runTest { + val isShelfAlignedToEnd by collectLastValue(underTest.isAlignedToEnd) + + kosmos.enableDualShade(wideLayout = false) + + assertThat(isShelfAlignedToEnd).isFalse() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt index d14ff35f824a..e5cb0fbc9e4b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt @@ -49,6 +49,7 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { private val sectionsManager = mock<NotificationSectionsManager>() private val msdlPlayer = kosmos.fakeMSDLPlayer private var canRowBeDismissed = true + private var magneticAnimationsCancelled = false private val underTest = kosmos.magneticNotificationRowManagerImpl @@ -64,6 +65,7 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { children = notificationTestHelper.createGroup(childrenNumber).childrenContainer swipedRow = children.attachedChildren[childrenNumber / 2] configureMagneticRowListener(swipedRow) + magneticAnimationsCancelled = false } @Test @@ -247,6 +249,35 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { assertThat(underTest.currentState).isEqualTo(State.IDLE) } + @Test + fun onMagneticInteractionEnd_whenDetached_cancelsMagneticAnimations() = + kosmos.testScope.runTest { + // GIVEN the swiped row is detached + setDetachedState() + + // WHEN the interaction ends on the row + underTest.onMagneticInteractionEnd(swipedRow, velocity = null) + + // THEN magnetic animations are cancelled + assertThat(magneticAnimationsCancelled).isTrue() + } + + @Test + fun onMagneticInteractionEnd_forMagneticNeighbor_cancelsMagneticAnimations() = + kosmos.testScope.runTest { + val neighborRow = children.attachedChildren[childrenNumber / 2 - 1] + configureMagneticRowListener(neighborRow) + + // GIVEN that targets are set + setTargets() + + // WHEN the interactionEnd is called on a target different from the swiped row + underTest.onMagneticInteractionEnd(neighborRow, null) + + // THEN magnetic animations are cancelled + assertThat(magneticAnimationsCancelled).isTrue() + } + private fun setDetachedState() { val threshold = 100f underTest.setSwipeThresholdPx(threshold) @@ -284,7 +315,11 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { startVelocity: Float, ) {} - override fun cancelMagneticAnimations() {} + override fun cancelMagneticAnimations() { + magneticAnimationsCancelled = true + } + + override fun cancelTranslationAnimations() {} override fun canRowBeDismissed(): Boolean = canRowBeDismissed } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java index 766ae73cb49d..789701f5e4b0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java @@ -405,7 +405,7 @@ public class NotificationSwipeHelperTest extends SysuiTestCase { doNothing().when(mSwipeHelper).superSnapChild(mNotificationRow, 0, 0); mSwipeHelper.snapChild(mNotificationRow, 0, 0); - verify(mCallback, times(1)).onDragCancelledWithVelocity(mNotificationRow, 0); + verify(mCallback, times(1)).onDragCancelled(mNotificationRow); verify(mSwipeHelper, times(1)).superSnapChild(mNotificationRow, 0, 0); verify(mSwipeHelper, times(1)).handleMenuCoveredOrDismissed(); } @@ -416,7 +416,7 @@ public class NotificationSwipeHelperTest extends SysuiTestCase { doNothing().when(mSwipeHelper).superSnapChild(mNotificationRow, 10, 0); mSwipeHelper.snapChild(mNotificationRow, 10, 0); - verify(mCallback, times(1)).onDragCancelledWithVelocity(mNotificationRow, 0); + verify(mCallback, times(1)).onDragCancelled(mNotificationRow); verify(mSwipeHelper, times(1)).superSnapChild(mNotificationRow, 10, 0); verify(mSwipeHelper, times(0)).handleMenuCoveredOrDismissed(); } @@ -426,7 +426,7 @@ public class NotificationSwipeHelperTest extends SysuiTestCase { doNothing().when(mSwipeHelper).superSnapChild(mView, 10, 0); mSwipeHelper.snapChild(mView, 10, 0); - verify(mCallback).onDragCancelledWithVelocity(mView, 0); + verify(mCallback).onDragCancelled(mView); verify(mSwipeHelper, never()).superSnapChild(mView, 10, 0); } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index 2e12336f6e93..6f785a3731e1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.phone; import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN; import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE; +import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow; import static kotlinx.coroutines.test.TestCoroutineDispatchersKt.StandardTestDispatcher; import static org.junit.Assert.assertFalse; @@ -76,6 +77,7 @@ import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInte import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.bouncer.ui.BouncerView; import com.android.systemui.bouncer.ui.BouncerViewDelegate; +import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor; import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor; import com.android.systemui.dock.DockManager; import com.android.systemui.dreams.DreamOverlayStateController; @@ -171,6 +173,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Mock private SceneInteractor mSceneInteractor; @Mock private DismissCallbackRegistry mDismissCallbackRegistry; @Mock private BouncerInteractor mBouncerInteractor; + @Mock private CommunalSceneInteractor mCommunalSceneInteractor; private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; private PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback @@ -209,6 +212,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { .thenReturn(mNotificationShadeWindowView); when(mNotificationShadeWindowView.getWindowInsetsController()) .thenReturn(mWindowInsetsController); + when(mCommunalSceneInteractor.isIdleOnCommunal()).thenReturn(MutableStateFlow(false)); mStatusBarKeyguardViewManager = new StatusBarKeyguardViewManager( @@ -245,7 +249,8 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mExecutor, () -> mDeviceEntryInteractor, mDismissCallbackRegistry, - () -> mBouncerInteractor) { + () -> mBouncerInteractor, + mCommunalSceneInteractor) { @Override public ViewRootImpl getViewRootImpl() { return mViewRootImpl; @@ -749,7 +754,8 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mExecutor, () -> mDeviceEntryInteractor, mDismissCallbackRegistry, - () -> mBouncerInteractor) { + () -> mBouncerInteractor, + mCommunalSceneInteractor) { @Override public ViewRootImpl getViewRootImpl() { return mViewRootImpl; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt index 73c191b32393..c48287c32120 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.phone.ongoingcall.domain.interactor import android.app.PendingIntent +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -30,12 +31,12 @@ import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository import com.android.systemui.statusbar.gesture.swipeStatusBarAwayGestureHandler -import com.android.systemui.statusbar.notification.data.model.activeNotificationModel -import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel -import com.android.systemui.statusbar.notification.shared.CallType +import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel +import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState +import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.removeOngoingCallState import com.android.systemui.statusbar.window.fakeStatusBarWindowControllerStore import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest @@ -50,6 +51,7 @@ import org.mockito.kotlin.verify @SmallTest @RunWith(AndroidJUnit4::class) +@EnableFlags(StatusBarChipsModernization.FLAG_NAME) class OngoingCallInteractorTest : SysuiTestCase() { private val kosmos = Kosmos().useUnconfinedTestDispatcher() private val repository = kosmos.activeNotificationListRepository @@ -76,21 +78,13 @@ class OngoingCallInteractorTest : SysuiTestCase() { val testIntent: PendingIntent = mock() val testPromotedContent = PromotedNotificationContentModel.Builder("promotedCall").build() - repository.activeNotifications.value = - ActiveNotificationsStore.Builder() - .apply { - addIndividualNotif( - activeNotificationModel( - key = "promotedCall", - whenTime = 1000L, - callType = CallType.Ongoing, - statusBarChipIcon = testIconView, - contentIntent = testIntent, - promotedContent = testPromotedContent, - ) - ) - } - .build() + addOngoingCallState( + key = "promotedCall", + startTimeMs = 1000L, + statusBarChipIconView = testIconView, + contentIntent = testIntent, + promotedContent = testPromotedContent, + ) // Verify model is InCall and has the correct icon, intent, and promoted content. assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java) @@ -101,45 +95,13 @@ class OngoingCallInteractorTest : SysuiTestCase() { } @Test - fun ongoingCallNotification_emitsInCall() = - kosmos.runTest { - val latest by collectLastValue(underTest.ongoingCallState) - - repository.activeNotifications.value = - ActiveNotificationsStore.Builder() - .apply { - addIndividualNotif( - activeNotificationModel( - key = "notif1", - whenTime = 1000L, - callType = CallType.Ongoing, - ) - ) - } - .build() - - assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java) - } - - @Test fun notificationRemoved_emitsNoCall() = kosmos.runTest { val latest by collectLastValue(underTest.ongoingCallState) - repository.activeNotifications.value = - ActiveNotificationsStore.Builder() - .apply { - addIndividualNotif( - activeNotificationModel( - key = "notif1", - whenTime = 1000L, - callType = CallType.Ongoing, - ) - ) - } - .build() - - repository.activeNotifications.value = ActiveNotificationsStore() + addOngoingCallState(key = "testKey") + removeOngoingCallState(key = "testKey") + assertThat(latest).isInstanceOf(OngoingCallModel.NoCall::class.java) } @@ -149,19 +111,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = true val latest by collectLastValue(underTest.ongoingCallState) - repository.activeNotifications.value = - ActiveNotificationsStore.Builder() - .apply { - addIndividualNotif( - activeNotificationModel( - key = "notif1", - whenTime = 1000L, - callType = CallType.Ongoing, - uid = UID, - ) - ) - } - .build() + addOngoingCallState(uid = UID) assertThat(latest).isInstanceOf(OngoingCallModel.InCallWithVisibleApp::class.java) } @@ -172,19 +122,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false val latest by collectLastValue(underTest.ongoingCallState) - repository.activeNotifications.value = - ActiveNotificationsStore.Builder() - .apply { - addIndividualNotif( - activeNotificationModel( - key = "notif1", - whenTime = 1000L, - callType = CallType.Ongoing, - uid = UID, - ) - ) - } - .build() + addOngoingCallState(uid = UID) assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java) } @@ -196,19 +134,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { // Start with notification and app not visible kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false - repository.activeNotifications.value = - ActiveNotificationsStore.Builder() - .apply { - addIndividualNotif( - activeNotificationModel( - key = "notif1", - whenTime = 1000L, - callType = CallType.Ongoing, - uid = UID, - ) - ) - } - .build() + addOngoingCallState(uid = UID) assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java) // App becomes visible @@ -234,7 +160,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { kosmos.fakeStatusBarWindowControllerStore.defaultDisplay .ongoingProcessRequiresStatusBarVisible ) - postOngoingCallNotification() + addOngoingCallState() assertThat(isStatusBarRequired).isTrue() assertThat(requiresStatusBarVisibleInRepository).isTrue() @@ -256,9 +182,9 @@ class OngoingCallInteractorTest : SysuiTestCase() { .ongoingProcessRequiresStatusBarVisible ) - postOngoingCallNotification() + addOngoingCallState(key = "testKey") - repository.activeNotifications.value = ActiveNotificationsStore() + removeOngoingCallState(key = "testKey") assertThat(isStatusBarRequired).isFalse() assertThat(requiresStatusBarVisibleInRepository).isFalse() @@ -283,7 +209,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false - postOngoingCallNotification() + addOngoingCallState(uid = UID) assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java) assertThat(requiresStatusBarVisibleInRepository).isTrue() @@ -305,7 +231,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { clearInvocations(kosmos.swipeStatusBarAwayGestureHandler) // Set up notification but not in fullscreen kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false - postOngoingCallNotification() + addOngoingCallState() assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java) verify(kosmos.swipeStatusBarAwayGestureHandler, never()) @@ -319,7 +245,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { // Set up notification and fullscreen mode kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true - postOngoingCallNotification() + addOngoingCallState() assertThat(isGestureListeningEnabled).isTrue() verify(kosmos.swipeStatusBarAwayGestureHandler) @@ -333,7 +259,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { // Set up notification and fullscreen mode kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true - postOngoingCallNotification() + addOngoingCallState() clearInvocations(kosmos.swipeStatusBarAwayGestureHandler) @@ -360,7 +286,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { ) // Start with an ongoing call (which should set status bar required) - postOngoingCallNotification() + addOngoingCallState() assertThat(isStatusBarRequiredForOngoingCall).isTrue() assertThat(requiresStatusBarVisibleInRepository).isTrue() @@ -374,22 +300,6 @@ class OngoingCallInteractorTest : SysuiTestCase() { assertThat(requiresStatusBarVisibleInWindowController).isFalse() } - private fun postOngoingCallNotification() { - repository.activeNotifications.value = - ActiveNotificationsStore.Builder() - .apply { - addIndividualNotif( - activeNotificationModel( - key = "notif1", - whenTime = 1000L, - callType = CallType.Ongoing, - uid = UID, - ) - ) - } - .build() - } - companion object { private const val UID = 885 } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt index ffdebb3517e7..b9e1c2ff232f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.policy.ui.dialog import android.app.Dialog import android.content.Intent +import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -25,10 +26,10 @@ import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.animation.mockActivityTransitionAnimatorController import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.backgroundCoroutineContext import com.android.systemui.kosmos.mainCoroutineContext import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.activityStarter -import com.android.systemui.runOnMainThreadAndWaitForIdleSync import com.android.systemui.shade.data.repository.shadeDialogContextInteractor import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.systemUIDialogFactory @@ -49,6 +50,7 @@ import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) class ModesDialogDelegateTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope @@ -76,20 +78,28 @@ class ModesDialogDelegateTest : SysuiTestCase() { activityStarter, { kosmos.modesDialogViewModel }, mockDialogEventLogger, + kosmos.applicationCoroutineScope, kosmos.mainCoroutineContext, + kosmos.backgroundCoroutineContext, kosmos.shadeDialogContextInteractor, ) } @Test - fun launchFromDialog_whenDialogNotOpen() { - val intent: Intent = mock() + fun launchFromDialog_whenDialogNotOpen() = + testScope.runTest { + val intent: Intent = mock() - runOnMainThreadAndWaitForIdleSync { underTest.launchFromDialog(intent) } + underTest.launchFromDialog(intent) + runCurrent() - verify(activityStarter) - .startActivity(eq(intent), eq(true), eq<ActivityTransitionAnimator.Controller?>(null)) - } + verify(activityStarter) + .startActivity( + eq(intent), + eq(true), + eq<ActivityTransitionAnimator.Controller?>(null), + ) + } @Test fun launchFromDialog_whenDialogOpen() = @@ -97,29 +107,26 @@ class ModesDialogDelegateTest : SysuiTestCase() { val intent: Intent = mock() lateinit var dialog: Dialog - runOnMainThreadAndWaitForIdleSync { - kosmos.applicationCoroutineScope.launch { dialog = underTest.showDialog() } - runCurrent() - underTest.launchFromDialog(intent) - } + kosmos.applicationCoroutineScope.launch { dialog = underTest.showDialog() } + runCurrent() + underTest.launchFromDialog(intent) + runCurrent() verify(mockDialogTransitionAnimator) .createActivityTransitionController(any<Dialog>(), eq(null)) verify(activityStarter).startActivity(eq(intent), eq(true), eq(mockAnimationController)) - runOnMainThreadAndWaitForIdleSync { dialog.dismiss() } + dialog.dismiss() } @Test fun dismiss_clearsDialogReference() { - val dialog = runOnMainThreadAndWaitForIdleSync { underTest.createDialog() } + val dialog = underTest.createDialog() assertThat(underTest.currentDialog).isEqualTo(dialog) - runOnMainThreadAndWaitForIdleSync { - dialog.show() - dialog.dismiss() - } + dialog.show() + dialog.dismiss() assertThat(underTest.currentDialog).isNull() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt index fecf1fd2f222..36e18e653f20 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt @@ -20,9 +20,12 @@ import android.content.Context import android.content.res.Resources import android.hardware.devicestate.DeviceStateManager import android.os.PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD +import android.os.PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.R +import com.android.internal.util.LatencyTracker +import com.android.internal.util.LatencyTracker.ACTION_SWITCH_DISPLAY_UNFOLD import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryImpl import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractorImpl @@ -44,23 +47,26 @@ import com.android.systemui.power.shared.model.ScreenPowerState.SCREEN_OFF import com.android.systemui.power.shared.model.ScreenPowerState.SCREEN_ON import com.android.systemui.shared.system.SysUiStatsLog import com.android.systemui.statusbar.policy.FakeConfigurationController +import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.COOL_DOWN_DURATION import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.FOLDABLE_DEVICE_STATE_CLOSED import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.FOLDABLE_DEVICE_STATE_HALF_OPEN +import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.SCREEN_EVENT_TIMEOUT import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor import com.android.systemui.unfoldedDeviceState import com.android.systemui.util.animation.data.repository.fakeAnimationStatusRepository -import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.capture import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import java.util.Optional +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.asExecutor import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -72,7 +78,10 @@ import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verifyNoMoreInteractions @RunWith(AndroidJUnit4::class) @SmallTest @@ -88,6 +97,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { private val animationStatusRepository = kosmos.fakeAnimationStatusRepository private val keyguardInteractor = mock<KeyguardInteractor>() private val displaySwitchLatencyLogger = mock<DisplaySwitchLatencyLogger>() + private val latencyTracker = mock<LatencyTracker>() private val deviceStateManager = kosmos.deviceStateManager private val closedDeviceState = kosmos.foldedDeviceStateList.first() @@ -142,6 +152,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { displaySwitchLatencyLogger, systemClock, deviceStateManager, + latencyTracker, ) } @@ -195,6 +206,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { displaySwitchLatencyLogger, systemClock, deviceStateManager, + latencyTracker, ) displaySwitchLatencyTracker.start() @@ -370,6 +382,283 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { } } + @Test + fun unfoldingDevice_startsLatencyTracking() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + + verify(latencyTracker).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun foldingDevice_doesntTrackLatency() { + testScope.runTest { + setDeviceState(UNFOLDED) + displaySwitchLatencyTracker.start() + runCurrent() + + startFolding() + + verify(latencyTracker, never()).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun foldedState_doesntStartTrackingOnScreenOn() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + powerInteractor.setScreenPowerState(SCREEN_ON) + runCurrent() + + verify(latencyTracker, never()).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun unfoldingDevice_endsLatencyTrackingWhenTransitionStarts() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + unfoldTransitionProgressProvider.onTransitionStarted() + runCurrent() + + verify(latencyTracker).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun unfoldingDevice_animationsDisabled_endsLatencyTrackingWhenScreenOn() { + testScope.runTest { + animationStatusRepository.onAnimationStatusChanged(enabled = false) + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + powerInteractor.setScreenPowerState(SCREEN_ON) + runCurrent() + + verify(latencyTracker).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun unfoldingDevice_doesntEndLatencyTrackingWhenScreenOn() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + powerInteractor.setScreenPowerState(SCREEN_ON) + runCurrent() + + verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun unfoldingDevice_animationsDisabled_endsLatencyTrackingWhenDeviceGoesToSleep() { + testScope.runTest { + animationStatusRepository.onAnimationStatusChanged(enabled = false) + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + powerInteractor.setAsleepForTest(sleepReason = GO_TO_SLEEP_REASON_POWER_BUTTON) + runCurrent() + + verify(latencyTracker).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun displaySwitchInterrupted_cancelsTrackingWhenNewDeviceStateEmitted() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + finishFolding() + + verify(latencyTracker).onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD) + verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun displaySwitchInterrupted_cancelsTrackingForManyStateChanges() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + startUnfolding() + startFolding() + startUnfolding() + finishUnfolding() + + verify(latencyTracker).onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD) + verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun displaySwitchInterrupted_startsOneTrackingForManyStateChanges() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + startUnfolding() + startFolding() + startUnfolding() + + verify(latencyTracker, times(1)).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun interruptedDisplaySwitchFinished_inCoolDownPeriod_trackingDisabled() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + finishFolding() + + advanceTimeBy(COOL_DOWN_DURATION.minus(10.milliseconds)) + startUnfolding() + finishUnfolding() + + verify(latencyTracker, times(1)).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun interruptedDisplaySwitchFinished_coolDownPassed_trackingWorksAsUsual() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + finishFolding() + + advanceTimeBy(COOL_DOWN_DURATION.plus(10.milliseconds)) + startUnfolding() + finishUnfolding() + + verify(latencyTracker, times(2)).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + verify(latencyTracker).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun displaySwitchInterrupted_coolDownExtendedByStartEvents() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + advanceTimeBy(COOL_DOWN_DURATION.minus(10.milliseconds)) + startUnfolding() + advanceTimeBy(20.milliseconds) + + startFolding() + finishUnfolding() + + verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun displaySwitchInterrupted_coolDownExtendedByAnyEndEvent() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + startUnfolding() + advanceTimeBy(COOL_DOWN_DURATION - 10.milliseconds) + powerInteractor.setScreenPowerState(SCREEN_ON) + advanceTimeBy(20.milliseconds) + + startFolding() + finishUnfolding() + + verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun displaySwitchTimedOut_trackingCancelled() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + advanceTimeBy(SCREEN_EVENT_TIMEOUT + 10.milliseconds) + finishUnfolding() + + verify(latencyTracker).onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun foldingStarted_screenStillOn_eventSentOnlyAfterScreenSwitches() { + // can happen for both folding and unfolding (with animations off) but it's more likely to + // happen when folding as waiting for screen on is the default case then + testScope.runTest { + startInUnfoldedState(displaySwitchLatencyTracker) + setDeviceState(FOLDED) + powerInteractor.setScreenPowerState(SCREEN_ON) + runCurrent() + + verifyNoMoreInteractions(displaySwitchLatencyLogger) + + powerInteractor.setScreenPowerState(SCREEN_OFF) + runCurrent() + powerInteractor.setScreenPowerState(SCREEN_ON) + runCurrent() + + verify(displaySwitchLatencyLogger).log(any()) + } + } + + private suspend fun TestScope.startInFoldedState(tracker: DisplaySwitchLatencyTracker) { + setDeviceState(FOLDED) + tracker.start() + runCurrent() + } + + private suspend fun TestScope.startInUnfoldedState(tracker: DisplaySwitchLatencyTracker) { + setDeviceState(UNFOLDED) + tracker.start() + runCurrent() + } + + private suspend fun TestScope.startUnfolding() { + setDeviceState(HALF_FOLDED) + powerInteractor.setScreenPowerState(SCREEN_OFF) + runCurrent() + } + + private suspend fun TestScope.startFolding() { + setDeviceState(FOLDED) + powerInteractor.setScreenPowerState(SCREEN_OFF) + runCurrent() + } + + private fun TestScope.finishFolding() { + powerInteractor.setScreenPowerState(SCREEN_ON) + runCurrent() + } + + private fun TestScope.finishUnfolding() { + unfoldTransitionProgressProvider.onTransitionStarted() + runCurrent() + } + private suspend fun setDeviceState(state: DeviceState) { foldStateRepository.emit(state) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt index 3ca1f5c0dd30..bafa8cf05a7f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.user.data.repository import android.app.admin.devicePolicyManager +import android.content.Intent import android.content.pm.UserInfo import android.internal.statusbar.fakeStatusBarService import android.os.UserHandle @@ -27,6 +28,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher @@ -50,6 +52,8 @@ import org.mockito.Mock import org.mockito.Mockito.mock import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock @SmallTest @RunWith(AndroidJUnit4::class) @@ -172,6 +176,39 @@ class UserRepositoryImplTest : SysuiTestCase() { } @Test + fun userUnlockedFlow_tracksBroadcastedChanges() = + testScope.runTest { + val userHandle: UserHandle = mock() + underTest = create(testScope.backgroundScope) + val latest by collectLastValue(underTest.isUserUnlocked(userHandle)) + whenever(manager.isUserUnlocked(eq(userHandle))).thenReturn(false) + broadcastDispatcher.sendIntentToMatchingReceiversOnly( + context, + Intent(Intent.ACTION_USER_UNLOCKED), + ) + + assertThat(latest).isFalse() + + whenever(manager.isUserUnlocked(eq(userHandle))).thenReturn(true) + broadcastDispatcher.sendIntentToMatchingReceiversOnly( + context, + Intent(Intent.ACTION_USER_UNLOCKED), + ) + + assertThat(latest).isTrue() + } + + @Test + fun userUnlockedFlow_initialValueReported() = + testScope.runTest { + val userHandle: UserHandle = mock() + underTest = create(testScope.backgroundScope) + whenever(manager.isUserUnlocked(eq(userHandle))).thenReturn(true) + val latest by collectLastValue(underTest.isUserUnlocked(userHandle)) + assertThat(latest).isTrue() + } + + @Test fun refreshUsers_sortsByCreationTime_guestUserLast() = testScope.runTest { underTest = create(testScope.backgroundScope) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/FlowTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/FlowTest.kt new file mode 100644 index 000000000000..2ca3d2fb916b --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/FlowTest.kt @@ -0,0 +1,100 @@ +/* + * 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.util.kotlin + +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.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class FlowTest : SysuiTestCase() { + val kosmos = testKosmos() + val testScope = kosmos.testScope + + @Test + fun combine6() { + testScope.runTest { + val result by collectLastValue(combine(f0, f1, f2, f3, f4, f5, ::listOf6)) + assertItemsEqualIndices(result) + } + } + + @Test + fun combine7() { + testScope.runTest { + val result by collectLastValue(combine(f0, f1, f2, f3, f4, f5, f6, ::listOf7)) + assertItemsEqualIndices(result) + } + } + + @Test + fun combine8() { + testScope.runTest { + val result by collectLastValue(combine(f0, f1, f2, f3, f4, f5, f6, f7, ::listOf8)) + assertItemsEqualIndices(result) + } + } + + @Test + fun combine9() { + testScope.runTest { + val result by collectLastValue(combine(f0, f1, f2, f3, f4, f5, f6, f7, f8, ::listOf9)) + assertItemsEqualIndices(result) + } + } + + private fun assertItemsEqualIndices(list: List<Int>?) { + assertThat(list).isNotNull() + list ?: return + + for (index in list.indices) { + assertThat(list[index]).isEqualTo(index) + } + } + + private val f0: Flow<Int> = flowOf(0) + private val f1: Flow<Int> = flowOf(1) + private val f2: Flow<Int> = flowOf(2) + private val f3: Flow<Int> = flowOf(3) + private val f4: Flow<Int> = flowOf(4) + private val f5: Flow<Int> = flowOf(5) + private val f6: Flow<Int> = flowOf(6) + private val f7: Flow<Int> = flowOf(7) + private val f8: Flow<Int> = flowOf(8) +} + +private fun <T> listOf6(a0: T, a1: T, a2: T, a3: T, a4: T, a5: T): List<T> = + listOf(a0, a1, a2, a3, a4, a5) + +private fun <T> listOf7(a0: T, a1: T, a2: T, a3: T, a4: T, a5: T, a6: T): List<T> = + listOf(a0, a1, a2, a3, a4, a5, a6) + +private fun <T> listOf8(a0: T, a1: T, a2: T, a3: T, a4: T, a5: T, a6: T, a7: T): List<T> = + listOf(a0, a1, a2, a3, a4, a5, a6, a7) + +private fun <T> listOf9(a0: T, a1: T, a2: T, a3: T, a4: T, a5: T, a6: T, a7: T, a8: T): List<T> = + listOf(a0, a1, a2, a3, a4, a5, a6, a7, a8) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java index 75f3386ed695..b8e19248b2de 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java @@ -47,6 +47,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.keyguard.TestScopeProvider; +import com.android.settingslib.volume.MediaSessions; import com.android.systemui.SysuiTestCase; import com.android.systemui.SysuiTestCaseExtKt; import com.android.systemui.broadcast.BroadcastDispatcher; @@ -268,13 +269,15 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { @Test public void testOnRemoteVolumeChanged_newStream_noNullPointer() { MediaSession.Token token = new MediaSession.Token(Process.myUid(), null); - mVolumeController.mMediaSessionsCallbacksW.onRemoteVolumeChanged(token, 0); + var sessionId = MediaSessions.SessionId.Companion.from(token); + mVolumeController.mMediaSessionsCallbacksW.onRemoteVolumeChanged(sessionId, 0); } @Test public void testOnRemoteRemove_newStream_noNullPointer() { MediaSession.Token token = new MediaSession.Token(Process.myUid(), null); - mVolumeController.mMediaSessionsCallbacksW.onRemoteRemoved(token); + var sessionId = MediaSessions.SessionId.Companion.from(token); + mVolumeController.mMediaSessionsCallbacksW.onRemoteRemoved(sessionId); } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt index 61ee5e04afd9..390518f3e2e5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt @@ -16,8 +16,10 @@ package com.android.systemui.window.ui.viewmodel +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope @@ -32,6 +34,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@EnableFlags(Flags.FLAG_BOUNCER_UI_REVAMP) class WindowRootViewModelTest : SysuiTestCase() { val kosmos = testKosmos() val testScope = kosmos.testScope diff --git a/packages/SystemUI/res/drawable/clipboard_minimized_background_inset.xml b/packages/SystemUI/res/drawable/clipboard_minimized_background_inset.xml new file mode 100644 index 000000000000..1ba637f379c1 --- /dev/null +++ b/packages/SystemUI/res/drawable/clipboard_minimized_background_inset.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<inset + xmlns:android="http://schemas.android.com/apk/res/android" + android:drawable="@drawable/clipboard_minimized_background" + android:inset="4dp"/>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml index 91cd019c85d1..43808f215a81 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml @@ -149,9 +149,9 @@ style="@style/TextAppearance.AuthCredential.Indicator" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginTop="24dp" android:layout_marginHorizontal="24dp" - android:accessibilityLiveRegion="assertive" + android:layout_marginTop="24dp" + android:accessibilityLiveRegion="polite" android:fadingEdge="horizontal" android:gravity="center_horizontal" android:scrollHorizontally="true" diff --git a/packages/SystemUI/res/layout/clipboard_overlay.xml b/packages/SystemUI/res/layout/clipboard_overlay.xml index 448b3e7d5ea0..915563b1ae20 100644 --- a/packages/SystemUI/res/layout/clipboard_overlay.xml +++ b/packages/SystemUI/res/layout/clipboard_overlay.xml @@ -171,12 +171,12 @@ android:layout_height="wrap_content" android:visibility="gone" android:elevation="7dp" - android:padding="8dp" + android:padding="12dp" app:layout_constraintBottom_toTopOf="@id/indication_container" app:layout_constraintStart_toStartOf="parent" - android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal" - android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom" - android:background="@drawable/clipboard_minimized_background"> + android:layout_marginStart="4dp" + android:layout_marginBottom="2dp" + android:background="@drawable/clipboard_minimized_background_inset"> <ImageView android:src="@drawable/ic_content_paste" android:tint="?attr/overlayButtonTextColor" diff --git a/packages/SystemUI/res/layout/ongoing_activity_chip_content.xml b/packages/SystemUI/res/layout/ongoing_activity_chip_content.xml index 6f42286d9fac..b66a88a3e523 100644 --- a/packages/SystemUI/res/layout/ongoing_activity_chip_content.xml +++ b/packages/SystemUI/res/layout/ongoing_activity_chip_content.xml @@ -43,9 +43,6 @@ ongoing_activity_chip_short_time_delta] will ever be shown at one time. --> <!-- Shows a timer, like 00:01. --> - <!-- Don't use the LimitedWidth style for the timer because the end of the timer is often - the most important value. ChipChronometer has the correct logic for when the timer is - too large for the space allowed. --> <com.android.systemui.statusbar.chips.ui.view.ChipChronometer android:id="@+id/ongoing_activity_chip_time" style="@style/StatusBar.Chip.Text" @@ -54,14 +51,14 @@ <!-- Shows generic text. --> <com.android.systemui.statusbar.chips.ui.view.ChipTextView android:id="@+id/ongoing_activity_chip_text" - style="@style/StatusBar.Chip.Text.LimitedWidth" + style="@style/StatusBar.Chip.Text" android:visibility="gone" /> <!-- Shows a time delta in short form, like "15min" or "1hr". --> <com.android.systemui.statusbar.chips.ui.view.ChipDateTimeView android:id="@+id/ongoing_activity_chip_short_time_delta" - style="@style/StatusBar.Chip.Text.LimitedWidth" + style="@style/StatusBar.Chip.Text" android:visibility="gone" /> diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml index 67f620f6fc54..8ad99abccdfe 100644 --- a/packages/SystemUI/res/layout/volume_dialog.xml +++ b/packages/SystemUI/res/layout/volume_dialog.xml @@ -16,7 +16,7 @@ <androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" - android:id="@+id/volume_dialog_root" + android:id="@+id/volume_dialog" android:layout_width="match_parent" android:layout_height="match_parent" android:alpha="0" diff --git a/packages/SystemUI/res/layout/volume_ringer_button.xml b/packages/SystemUI/res/layout/volume_ringer_button.xml index 6748cfa05c35..4e3c8cc4413b 100644 --- a/packages/SystemUI/res/layout/volume_ringer_button.xml +++ b/packages/SystemUI/res/layout/volume_ringer_button.xml @@ -13,20 +13,13 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" +<ImageButton xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" - android:layout_width="wrap_content" - android:layout_height="wrap_content" > - - <ImageButton - android:id="@+id/volume_drawer_button" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:padding="@dimen/volume_dialog_ringer_drawer_button_icon_radius" - android:contentDescription="@string/volume_ringer_mode" - android:gravity="center" - android:tint="@androidprv:color/materialColorOnSurface" - android:src="@drawable/volume_ringer_item_bg" - android:background="@drawable/volume_ringer_item_bg"/> - -</FrameLayout> + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@drawable/volume_ringer_item_bg" + android:contentDescription="@string/volume_ringer_mode" + android:gravity="center" + android:padding="@dimen/volume_dialog_ringer_drawer_button_icon_radius" + android:src="@drawable/volume_ringer_item_bg" + android:tint="@androidprv:color/materialColorOnSurface" /> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 9b8926e921c9..09aa2241e42b 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -1110,4 +1110,7 @@ <!-- Configuration for wallpaper focal area --> <bool name="center_align_focal_area_shape">false</bool> <string name="focal_area_target" translatable="false" /> + + <!-- Configuration to swipe to open glanceable hub --> + <bool name="config_swipeToOpenGlanceableHub">false</bool> </resources> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 2d3c07b93cb1..648e4c2e3ac7 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1811,6 +1811,7 @@ <dimen name="ongoing_activity_chip_text_end_padding_for_embedded_padding_icon">6dp</dimen> <dimen name="ongoing_activity_chip_text_fading_edge_length">12dp</dimen> <dimen name="ongoing_activity_chip_corner_radius">28dp</dimen> + <dimen name="ongoing_activity_chip_outline_width">2px</dimen> <!-- Status bar user chip --> <dimen name="status_bar_user_chip_avatar_size">16dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index d18a90a17abe..86292039d93d 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1351,6 +1351,10 @@ <string name="accessibility_action_label_shrink_widget">Decrease height</string> <!-- Label for accessibility action to expand a widget in edit mode. [CHAR LIMIT=NONE] --> <string name="accessibility_action_label_expand_widget">Increase height</string> + <!-- Label for accessibility action to show the next media player. [CHAR LIMIT=NONE] --> + <string name="accessibility_action_label_umo_show_next">Show next</string> + <!-- Label for accessibility action to show the previous media player. [CHAR LIMIT=NONE] --> + <string name="accessibility_action_label_umo_show_previous">Show previous</string> <!-- Title shown above information regarding lock screen widgets. [CHAR LIMIT=50] --> <string name="communal_widgets_disclaimer_title">Lock screen widgets</string> <!-- Information about lock screen widgets presented to the user. [CHAR LIMIT=NONE] --> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 5ef4d4014ba6..4961a7ece69a 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -93,15 +93,6 @@ <item name="android:textColor">?android:attr/colorPrimary</item> </style> - <!-- Style for a status bar chip text that has a maximum width. Since there's so little room in - the status bar chip area, don't ellipsize the text and instead just fade it out a bit at - the end. --> - <style name="StatusBar.Chip.Text.LimitedWidth"> - <item name="android:ellipsize">none</item> - <item name="android:requiresFadingEdge">horizontal</item> - <item name="android:fadingEdgeLength">@dimen/ongoing_activity_chip_text_fading_edge_length</item> - </style> - <style name="Chipbar" /> <style name="Chipbar.Text" parent="@*android:style/TextAppearance.DeviceDefault.Notification.Title"> @@ -258,7 +249,7 @@ <style name="TextAppearance.AuthNonBioCredential.Title"> <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item> <item name="android:layout_marginTop">24dp</item> - <item name="android:textSize">36dp</item> + <item name="android:textSize">36sp</item> <item name="android:focusable">true</item> <item name="android:textColor">@androidprv:color/materialColorOnSurface</item> </style> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java index 335a910eb106..73dc28230e65 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java @@ -98,7 +98,6 @@ import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.settingslib.Utils; import com.android.settingslib.drawable.CircleFramedDrawable; -import com.android.systemui.Flags; import com.android.systemui.FontStyles; import com.android.systemui.Gefingerpoken; import com.android.systemui.classifier.FalsingA11yDelegate; @@ -121,6 +120,7 @@ public class KeyguardSecurityContainer extends ConstraintLayout { static final int USER_TYPE_PRIMARY = 1; static final int USER_TYPE_WORK_PROFILE = 2; static final int USER_TYPE_SECONDARY_USER = 3; + private boolean mTransparentModeEnabled = false; @IntDef({MODE_UNINITIALIZED, MODE_DEFAULT, MODE_ONE_HANDED, MODE_USER_SWITCHER}) public @interface Mode {} @@ -814,15 +814,30 @@ public class KeyguardSecurityContainer extends ConstraintLayout { mDisappearAnimRunning = false; } + /** + * Make the bouncer background transparent + */ + public void enableTransparentMode() { + mTransparentModeEnabled = true; + reloadBackgroundColor(); + } + + /** + * Make the bouncer background opaque + */ + public void disableTransparentMode() { + mTransparentModeEnabled = false; + reloadBackgroundColor(); + } + private void reloadBackgroundColor() { - if (Flags.bouncerUiRevamp()) { - // Keep the background transparent, otherwise the background color looks like a box - // while scaling the bouncer for back animation or while transitioning to the bouncer. + if (mTransparentModeEnabled) { setBackgroundColor(Color.TRANSPARENT); } else { setBackgroundColor( getContext().getColor(com.android.internal.R.color.materialColorSurfaceDim)); } + invalidate(); } void reloadColors() { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index ff7b2b025539..d10fce416150 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -70,6 +70,7 @@ import com.android.keyguard.KeyguardSecurityContainer.SwipeListener; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.keyguard.dagger.KeyguardBouncerScope; import com.android.settingslib.utils.ThreadUtils; +import com.android.systemui.Flags; import com.android.systemui.Gefingerpoken; import com.android.systemui.biometrics.FaceAuthAccessibilityDelegate; import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor; @@ -96,6 +97,8 @@ import com.android.systemui.user.domain.interactor.SelectedUserInteractor; import com.android.systemui.util.ViewController; import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.util.settings.GlobalSettings; +import com.android.systemui.window.data.repository.WindowRootViewBlurRepository; +import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor; import dagger.Lazy; @@ -134,6 +137,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard private final FalsingA11yDelegate mFalsingA11yDelegate; private final DeviceEntryFaceAuthInteractor mDeviceEntryFaceAuthInteractor; private final BouncerMessageInteractor mBouncerMessageInteractor; + private final Lazy<WindowRootViewBlurInteractor> mRootViewBlurInteractor; private int mTranslationY; private final KeyguardDismissTransitionInteractor mKeyguardDismissTransitionInteractor; private final DevicePolicyManager mDevicePolicyManager; @@ -431,6 +435,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard private final Executor mBgExecutor; @Nullable private Job mSceneTransitionCollectionJob; + private Job mBlurEnabledCollectionJob; @Inject public KeyguardSecurityContainerController(KeyguardSecurityContainer view, @@ -463,9 +468,11 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard KeyguardDismissTransitionInteractor keyguardDismissTransitionInteractor, Lazy<PrimaryBouncerInteractor> primaryBouncerInteractor, @Background Executor bgExecutor, - Provider<DeviceEntryInteractor> deviceEntryInteractor + Provider<DeviceEntryInteractor> deviceEntryInteractor, + Lazy<WindowRootViewBlurInteractor> rootViewBlurInteractorProvider ) { super(view); + mRootViewBlurInteractor = rootViewBlurInteractorProvider; view.setAccessibilityDelegate(faceAuthAccessibilityDelegate); mLockPatternUtils = lockPatternUtils; mUpdateMonitor = keyguardUpdateMonitor; @@ -539,6 +546,32 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard } ); } + + if (Flags.bouncerUiRevamp()) { + mBlurEnabledCollectionJob = mJavaAdapter.get().alwaysCollectFlow( + mRootViewBlurInteractor.get().isBlurCurrentlySupported(), + this::handleBlurSupportedChanged); + } + } + + private void handleBlurSupportedChanged(boolean isWindowBlurSupported) { + if (isWindowBlurSupported) { + mView.enableTransparentMode(); + } else { + mView.disableTransparentMode(); + } + } + + private void refreshBouncerBackground() { + // This is present solely for screenshot tests that disable blur by invoking setprop to + // disable blurs, however the mRootViewBlurInteractor#isBlurCurrentlySupported doesn't emit + // an updated value because sysui doesn't have a way to register for changes to setprop. + // KeyguardSecurityContainer view is inflated only once and doesn't re-inflate so it has to + // check the sysprop every time bouncer is about to be shown. + if (Flags.bouncerUiRevamp() && (ActivityManager.isRunningInUserTestHarness() + || ActivityManager.isRunningInTestHarness())) { + handleBlurSupportedChanged(!WindowRootViewBlurRepository.isDisableBlurSysPropSet()); + } } @Override @@ -552,6 +585,11 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard mSceneTransitionCollectionJob.cancel(null); mSceneTransitionCollectionJob = null; } + + if (mBlurEnabledCollectionJob != null) { + mBlurEnabledCollectionJob.cancel(null); + mBlurEnabledCollectionJob = null; + } } /** */ @@ -718,6 +756,8 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard if (bouncerUserSwitcher != null) { bouncerUserSwitcher.setAlpha(0f); } + + refreshBouncerBackground(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java index f835ad689132..e2065f175c79 100644 --- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java +++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java @@ -352,6 +352,7 @@ public class SwipeHelper implements Gefingerpoken, Dumpable { && Math.abs(delta) > Math.abs(deltaPerpendicular)) { if (mCallback.canChildBeDragged(mTouchedView)) { mIsSwiping = true; + mCallback.setMagneticAndRoundableTargets(mTouchedView); mCallback.onBeginDrag(mTouchedView); mInitialTouchPos = getPos(ev); mTranslation = getTranslation(mTouchedView); @@ -444,6 +445,7 @@ public class SwipeHelper implements Gefingerpoken, Dumpable { }; Animator anim = getViewTranslationAnimator(animView, newPos, updateListener); + mCallback.onMagneticInteractionEnd(animView, velocity); if (anim == null) { onDismissChildWithAnimationFinished(); return; @@ -733,7 +735,8 @@ public class SwipeHelper implements Gefingerpoken, Dumpable { dismissChild(mTouchedView, velocity, !swipedFastEnough() /* useAccelerateInterpolator */); } else { - mCallback.onDragCancelledWithVelocity(mTouchedView, velocity); + mCallback.onMagneticInteractionEnd(mTouchedView, velocity); + mCallback.onDragCancelled(mTouchedView); snapChild(mTouchedView, 0 /* leftTarget */, velocity); } mTouchedView = null; @@ -935,18 +938,24 @@ public class SwipeHelper implements Gefingerpoken, Dumpable { void onBeginDrag(View v); + /** + * Set magnetic and roundable targets for a view. + */ + void setMagneticAndRoundableTargets(View v); + void onChildDismissed(View v); void onDragCancelled(View v); /** - * A drag operation has been cancelled on a view with a final velocity. - * @param v View that was dragged. - * @param finalVelocity Final velocity of the drag. + * Notify that a magnetic interaction ended on a view with a velocity. + * <p> + * This method should be called when a view will snap back or be dismissed. + * + * @param view The {@link View} whose magnetic interaction ended. + * @param velocity The velocity when the interaction ended. */ - default void onDragCancelledWithVelocity(View v, float finalVelocity) { - onDragCancelled(v); - } + void onMagneticInteractionEnd(View view, float velocity); /** * Called when the child is long pressed and available to start drag and drop. diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/OWNERS b/packages/SystemUI/src/com/android/systemui/accessibility/OWNERS index 1ed8c068f974..5a59b7aaef56 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/OWNERS +++ b/packages/SystemUI/src/com/android/systemui/accessibility/OWNERS @@ -1,4 +1,7 @@ -# Bug component: 44215 +# Bug component: 1530954 +# +# The above component is for automated test bugs. If you are a human looking to report +# a bug in this codebase then please use component 44215. include /core/java/android/view/accessibility/OWNERS jonesriley@google.com
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS b/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS index b65d29c6a0bb..429b4b0fccab 100644 --- a/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS +++ b/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS @@ -5,5 +5,4 @@ linyuh@google.com pauldpong@google.com praveenj@google.com vicliang@google.com -mfolkerts@google.com yuklimko@google.com diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt index e365b770c203..d8e7a168ef3c 100644 --- a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt @@ -43,6 +43,7 @@ import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.ui.view.WindowRootView import com.android.systemui.shade.ShadeExpansionChangeEvent +import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.phone.CentralSurfaces import com.android.wm.shell.animation.FlingAnimationUtils @@ -79,6 +80,7 @@ constructor( private val activityStarter: ActivityStarter, private val keyguardInteractor: KeyguardInteractor, private val sceneInteractor: SceneInteractor, + private val shadeRepository: ShadeRepository, private val windowRootViewProvider: Optional<Provider<WindowRootView>>, ) : TouchHandler { /** An interface for creating ValueAnimators. */ @@ -260,6 +262,8 @@ constructor( } scrimManager.addCallback(scrimManagerCallback) currentScrimController = scrimManager.currentController + + shadeRepository.setLegacyShadeTracking(true) session.registerCallback { velocityTracker?.apply { recycle() } velocityTracker = null @@ -270,6 +274,7 @@ constructor( if (!Flags.communalBouncerDoNotModifyPluginOpen()) { notificationShadeWindowController.setForcePluginOpen(false, this) } + shadeRepository.setLegacyShadeTracking(false) } session.registerGestureListener(onGestureListener) session.registerInputListener { ev: InputEvent -> onMotionEvent(ev) } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt index 6cd763a9d3d0..bbf9a19012a4 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt @@ -31,6 +31,7 @@ import com.airbnb.lottie.LottieAnimationView import com.airbnb.lottie.LottieComposition import com.airbnb.lottie.LottieProperty import com.android.app.animation.Interpolators +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.keyguard.KeyguardPINView import com.android.systemui.CoreStartable import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractor @@ -50,7 +51,6 @@ import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.combine -import com.android.app.tracing.coroutines.launchTraced as launch /** Binds the side fingerprint sensor indicator view to [SideFpsOverlayViewModel]. */ @SysUISingleton @@ -65,51 +65,53 @@ constructor( private val layoutInflater: Lazy<LayoutInflater>, private val sideFpsProgressBarViewModel: Lazy<SideFpsProgressBarViewModel>, private val sfpsSensorInteractor: Lazy<SideFpsSensorInteractor>, - private val windowManager: Lazy<WindowManager> + private val windowManager: Lazy<WindowManager>, ) : CoreStartable { override fun start() { - applicationScope - .launch { - sfpsSensorInteractor.get().isAvailable.collect { isSfpsAvailable -> - if (isSfpsAvailable) { - combine( - biometricStatusInteractor.get().sfpsAuthenticationReason, - deviceEntrySideFpsOverlayInteractor - .get() - .showIndicatorForDeviceEntry, - sideFpsProgressBarViewModel.get().isVisible, - ::Triple + applicationScope.launch { + sfpsSensorInteractor.get().isAvailable.collect { isSfpsAvailable -> + if (isSfpsAvailable) { + combine( + biometricStatusInteractor.get().sfpsAuthenticationReason, + deviceEntrySideFpsOverlayInteractor.get().showIndicatorForDeviceEntry, + sideFpsProgressBarViewModel.get().isVisible, + ::Triple, + ) + .sample(displayStateInteractor.get().isInRearDisplayMode, ::Pair) + .collect { (combinedFlows, isInRearDisplayMode: Boolean) -> + val ( + systemServerAuthReason, + showIndicatorForDeviceEntry, + progressBarIsVisible) = + combinedFlows + Log.d( + TAG, + "systemServerAuthReason = $systemServerAuthReason, " + + "showIndicatorForDeviceEntry = " + + "$showIndicatorForDeviceEntry, " + + "progressBarIsVisible = $progressBarIsVisible", ) - .sample(displayStateInteractor.get().isInRearDisplayMode, ::Pair) - .collect { (combinedFlows, isInRearDisplayMode: Boolean) -> - val ( - systemServerAuthReason, - showIndicatorForDeviceEntry, - progressBarIsVisible) = - combinedFlows - Log.d( - TAG, - "systemServerAuthReason = $systemServerAuthReason, " + - "showIndicatorForDeviceEntry = " + - "$showIndicatorForDeviceEntry, " + - "progressBarIsVisible = $progressBarIsVisible" - ) - if (!isInRearDisplayMode) { - if (progressBarIsVisible) { - hide() - } else if (systemServerAuthReason != NotRunning) { - show() - } else if (showIndicatorForDeviceEntry) { - show() - } else { - hide() - } + if (!isInRearDisplayMode) { + if (progressBarIsVisible) { + hide() + } else if (systemServerAuthReason != NotRunning) { + show() + } else if (showIndicatorForDeviceEntry) { + show() + overlayView?.announceForAccessibility( + applicationContext.resources.getString( + R.string.accessibility_side_fingerprint_indicator_label + ) + ) + } else { + hide() } } - } + } } } + } } private var overlayView: View? = null @@ -119,7 +121,7 @@ constructor( if (overlayView?.isAttachedToWindow == true) { Log.d( TAG, - "show(): overlayView $overlayView isAttachedToWindow already, ignoring show request" + "show(): overlayView $overlayView isAttachedToWindow already, ignoring show request", ) return } @@ -137,11 +139,6 @@ constructor( overlayView!!.visibility = View.INVISIBLE Log.d(TAG, "show(): adding overlayView $overlayView") windowManager.get().addView(overlayView, overlayViewModel.defaultOverlayViewParams) - overlayView!!.announceForAccessibility( - applicationContext.resources.getString( - R.string.accessibility_side_fingerprint_indicator_label - ) - ) } /** Hide the side fingerprint sensor indicator */ @@ -163,7 +160,7 @@ constructor( fun bind( overlayView: View, viewModel: SideFpsOverlayViewModel, - windowManager: WindowManager + windowManager: WindowManager, ) { overlayView.repeatWhenAttached { val lottie = it.requireViewById<LottieAnimationView>(R.id.sidefps_animation) @@ -186,7 +183,7 @@ constructor( object : View.AccessibilityDelegate() { override fun dispatchPopulateAccessibilityEvent( host: View, - event: AccessibilityEvent + event: AccessibilityEvent, ): Boolean { return if ( event.getEventType() === diff --git a/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java b/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java index 2e1b5ad177b5..e456310febfd 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java +++ b/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java @@ -17,16 +17,19 @@ package com.android.systemui.communal; import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP; -import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_GOING_TO_SLEEP; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.systemui.dagger.qualifiers.Application; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; +import com.android.systemui.keyguard.shared.model.DozeStateModel; import com.android.systemui.shared.condition.Condition; import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.util.kotlin.JavaAdapter; import kotlinx.coroutines.CoroutineScope; +import kotlinx.coroutines.Job; import javax.inject.Inject; @@ -38,6 +41,10 @@ public class DeviceInactiveCondition extends Condition { private final KeyguardStateController mKeyguardStateController; private final WakefulnessLifecycle mWakefulnessLifecycle; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; + private final KeyguardInteractor mKeyguardInteractor; + private final JavaAdapter mJavaAdapter; + private Job mAnyDozeListenerJob; + private boolean mAnyDoze; private final KeyguardStateController.Callback mKeyguardStateCallback = new KeyguardStateController.Callback() { @Override @@ -63,12 +70,14 @@ public class DeviceInactiveCondition extends Condition { @Inject public DeviceInactiveCondition(@Application CoroutineScope scope, KeyguardStateController keyguardStateController, - WakefulnessLifecycle wakefulnessLifecycle, - KeyguardUpdateMonitor keyguardUpdateMonitor) { + WakefulnessLifecycle wakefulnessLifecycle, KeyguardUpdateMonitor keyguardUpdateMonitor, + KeyguardInteractor keyguardInteractor, JavaAdapter javaAdapter) { super(scope); mKeyguardStateController = keyguardStateController; mWakefulnessLifecycle = wakefulnessLifecycle; mKeyguardUpdateMonitor = keyguardUpdateMonitor; + mKeyguardInteractor = keyguardInteractor; + mJavaAdapter = javaAdapter; } @Override @@ -77,6 +86,11 @@ public class DeviceInactiveCondition extends Condition { mKeyguardStateController.addCallback(mKeyguardStateCallback); mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateCallback); mWakefulnessLifecycle.addObserver(mWakefulnessObserver); + mAnyDozeListenerJob = mJavaAdapter.alwaysCollectFlow( + mKeyguardInteractor.getDozeTransitionModel(), dozeModel -> { + mAnyDoze = !DozeStateModel.Companion.isDozeOff(dozeModel.getTo()); + updateState(); + }); } @Override @@ -84,6 +98,7 @@ public class DeviceInactiveCondition extends Condition { mKeyguardStateController.removeCallback(mKeyguardStateCallback); mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateCallback); mWakefulnessLifecycle.removeObserver(mWakefulnessObserver); + mAnyDozeListenerJob.cancel(null); } @Override @@ -92,10 +107,10 @@ public class DeviceInactiveCondition extends Condition { } private void updateState() { - final boolean asleep = - mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_ASLEEP - || mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_GOING_TO_SLEEP; - updateCondition(asleep || mKeyguardStateController.isShowing() - || mKeyguardUpdateMonitor.isDreaming()); + final boolean asleep = mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_ASLEEP; + // Doze/AoD is also a dream, but we should never override it with low light as to the user + // it's totally unrelated. + updateCondition(!mAnyDoze && (asleep || mKeyguardStateController.isShowing() + || mKeyguardUpdateMonitor.isDreaming())); } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt index f01a6dbf568f..ff741625a3cc 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt @@ -104,6 +104,7 @@ interface CommunalModule { companion object { const val LOGGABLE_PREFIXES = "loggable_prefixes" const val LAUNCHER_PACKAGE = "launcher_package" + const val SWIPE_TO_HUB = "swipe_to_hub" @Provides @Communal @@ -143,5 +144,11 @@ interface CommunalModule { fun provideLauncherPackage(@Main resources: Resources): String { return resources.getString(R.string.launcher_overlayable_package) } + + @Provides + @Named(SWIPE_TO_HUB) + fun provideSwipeToHub(@Main resources: Resources): Boolean { + return resources.getBoolean(R.bool.config_swipeToOpenGlanceableHub) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt index 3907a37cd5d9..f304b97a4ffe 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt @@ -37,11 +37,13 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog +import com.android.systemui.user.domain.interactor.UserLockedInteractor import javax.inject.Inject import javax.inject.Named import javax.inject.Provider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first /** * Callback that will be invoked when the Room database is created. Then the database will be @@ -57,6 +59,7 @@ constructor( @Named(DEFAULT_WIDGETS) private val defaultWidgets: Array<String>, @CommunalLog logBuffer: LogBuffer, private val userManager: UserManager, + private val userLockedInteractor: UserLockedInteractor, ) : RoomDatabase.Callback() { companion object { private const val TAG = "DefaultWidgetPopulation" @@ -79,38 +82,36 @@ constructor( } bgScope.launch { - // Default widgets should be associated with the main user. - val user = userManager.mainUser - - if (user == null) { - logger.w( - "Skipped populating default widgets. Reason: device does not have a main user" - ) - return@launch - } + userLockedInteractor.isUserUnlocked(userManager.mainUser).first { it } + populateDefaultWidgets() + } + } - val userSerialNumber = userManager.getUserSerialNumber(user.identifier) - - defaultWidgets.forEachIndexed { index, name -> - val provider = ComponentName.unflattenFromString(name) - provider?.let { - val id = communalWidgetHost.allocateIdAndBindWidget(provider, user) - id?.let { - communalWidgetDaoProvider - .get() - .addWidget( - widgetId = id, - componentName = name, - rank = index, - userSerialNumber = userSerialNumber, - spanY = SpanValue.Fixed(3), - ) - } + private fun populateDefaultWidgets() { + // Default widgets should be associated with the main user. + val user = userManager.mainUser ?: return + + val userSerialNumber = userManager.getUserSerialNumber(user.identifier) + + defaultWidgets.forEachIndexed { index, name -> + val provider = ComponentName.unflattenFromString(name) + provider?.let { + val id = communalWidgetHost.allocateIdAndBindWidget(provider, user) + id?.let { + communalWidgetDaoProvider + .get() + .addWidget( + widgetId = id, + componentName = name, + rank = index, + userSerialNumber = userSerialNumber, + spanY = SpanValue.Fixed(3), + ) } } - - logger.i("Populated default widgets in the database.") } + + logger.i("Populated default widgets in the database.") } /** 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 8b6322720118..0a9bd4214a12 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 @@ -147,5 +147,9 @@ constructor( to: OverlayKey, transitionKey: TransitionKey?, ) = Unit + + override fun instantlyShowOverlay(overlay: OverlayKey) = Unit + + override fun instantlyHideOverlay(overlay: OverlayKey) = Unit } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index de55c92e84f9..6dab32a66c94 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -69,6 +69,7 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.phone.ManagedProfileController +import com.android.systemui.user.domain.interactor.UserLockedInteractor import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf import com.android.systemui.util.kotlin.BooleanFlowOperators.not import com.android.systemui.util.kotlin.emitOnStart @@ -127,6 +128,7 @@ constructor( private val batteryInteractor: BatteryInteractor, private val dockManager: DockManager, private val posturingInteractor: PosturingInteractor, + private val userLockedInteractor: UserLockedInteractor, ) { private val logger = Logger(logBuffer, "CommunalInteractor") @@ -162,7 +164,7 @@ constructor( val isCommunalAvailable: Flow<Boolean> = allOf( communalSettingsInteractor.isCommunalEnabled, - not(keyguardInteractor.isEncryptedOrLockdown), + userLockedInteractor.isUserUnlocked(userManager.mainUser), keyguardInteractor.isKeyguardShowing, ) .distinctUntilChanged() 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 49003a735fbd..a4860dfc47ce 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 @@ -202,6 +202,12 @@ abstract class BaseCommunalViewModel( /** Called as the user request to show the customize widget button. */ open fun onLongClick() {} + /** Called as the user requests to switch to the previous player in UMO. */ + open fun onShowPreviousMedia() {} + + /** Called as the user requests to switch to the next player in UMO. */ + open fun onShowNextMedia() {} + /** Called as the UI determines that a new widget has been added to the grid. */ open fun onNewWidgetAdded(provider: AppWidgetProviderInfo) {} diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModel.kt index 29d9cacdbc79..be1d005a0198 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModel.kt @@ -20,7 +20,6 @@ import com.android.compose.animation.scene.Swipe import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor -import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel import com.android.systemui.shade.domain.interactor.ShadeInteractor @@ -60,8 +59,7 @@ constructor( if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer add(Swipe.Up to bouncerOrGone) - // "Home" is either Lockscreen, or Gone - if the device is entered. - add(Swipe.End to SceneFamilies.Home) + add(Swipe.End to Scenes.Lockscreen) addAll( when (shadeMode) { diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index 4bc44005d2fc..2169881d11c5 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -19,6 +19,7 @@ package com.android.systemui.communal.ui.viewmodel import android.content.ComponentName import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.Flags +import com.android.systemui.communal.dagger.CommunalModule.Companion.SWIPE_TO_HUB import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor @@ -92,6 +93,7 @@ constructor( private val metricsLogger: CommunalMetricsLogger, mediaCarouselController: MediaCarouselController, blurConfig: BlurConfig, + @Named(SWIPE_TO_HUB) private val swipeToHub: Boolean, ) : BaseCommunalViewModel( communalSceneInteractor, @@ -254,6 +256,14 @@ constructor( } } + override fun onShowPreviousMedia() { + mediaCarouselController.mediaCarouselScrollHandler.scrollByStep(-1) + } + + override fun onShowNextMedia() { + mediaCarouselController.mediaCarouselScrollHandler.scrollByStep(1) + } + override fun onTapWidget(componentName: ComponentName, rank: Int) { metricsLogger.logTapWidget(componentName.flattenToString(), rank) } @@ -349,6 +359,8 @@ constructor( /** See [CommunalSettingsInteractor.isV2FlagEnabled] */ fun v2FlagEnabled(): Boolean = communalSettingsInteractor.isV2FlagEnabled() + fun swipeToHubEnabled(): Boolean = swipeToHub + companion object { const val POPUP_AUTO_HIDE_TIMEOUT_MS = 12000L } diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt index dec7ba3a3eb1..06a14eaa5c85 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt @@ -26,6 +26,7 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.settings.UserTracker +import com.android.systemui.user.domain.interactor.UserLockedInteractor import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf import com.android.systemui.util.kotlin.BooleanFlowOperators.not @@ -58,6 +59,7 @@ constructor( @Main private val uiDispatcher: CoroutineDispatcher, private val glanceableHubWidgetManagerLazy: Lazy<GlanceableHubWidgetManager>, private val glanceableHubMultiUserHelper: GlanceableHubMultiUserHelper, + private val userLockedInteractor: UserLockedInteractor, ) : CoreStartable { private val appWidgetHost by lazy { appWidgetHostLazy.get() } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java index 3c68e3a09f02..8bff090959ab 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java @@ -74,6 +74,7 @@ import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.dagger.CentralSurfacesModule; import com.android.systemui.statusbar.dagger.StartCentralSurfacesModule; +import com.android.systemui.statusbar.notification.dagger.NotificationStackModule; import com.android.systemui.statusbar.notification.dagger.ReferenceNotificationsModule; import com.android.systemui.statusbar.notification.headsup.HeadsUpModule; import com.android.systemui.statusbar.phone.CentralSurfaces; @@ -169,6 +170,7 @@ import javax.inject.Named; WallpaperModule.class, ShortcutHelperModule.class, ContextualEducationModule.class, + NotificationStackModule.class, }) public abstract class ReferenceSystemUIModule { diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index fcc3ea9f7d58..fed77090c477 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -18,6 +18,7 @@ package com.android.systemui.dagger import com.android.keyguard.KeyguardBiometricLockoutLogger import com.android.systemui.CoreStartable +import com.android.systemui.Flags.unfoldLatencyTrackingFix import com.android.systemui.LatencyTester import com.android.systemui.SliceBroadcastRelayHandler import com.android.systemui.accessibility.Magnification @@ -60,6 +61,7 @@ import com.android.systemui.stylus.StylusUsiPowerStartable import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator import com.android.systemui.theme.ThemeOverlayController import com.android.systemui.unfold.DisplaySwitchLatencyTracker +import com.android.systemui.unfold.NoCooldownDisplaySwitchLatencyTracker import com.android.systemui.usb.StorageNotification import com.android.systemui.util.NotificationChannels import com.android.systemui.util.StartBinderLoggerModule @@ -67,8 +69,10 @@ import com.android.systemui.wallpapers.dagger.WallpaperModule import com.android.systemui.wmshell.WMShell import dagger.Binds import dagger.Module +import dagger.Provides import dagger.multibindings.ClassKey import dagger.multibindings.IntoMap +import javax.inject.Provider /** * DEPRECATED: DO NOT ADD THINGS TO THIS FILE. @@ -148,12 +152,6 @@ abstract class SystemUICoreStartableModule { @ClassKey(LatencyTester::class) abstract fun bindLatencyTester(sysui: LatencyTester): CoreStartable - /** Inject into DisplaySwitchLatencyTracker. */ - @Binds - @IntoMap - @ClassKey(DisplaySwitchLatencyTracker::class) - abstract fun bindDisplaySwitchLatencyTracker(sysui: DisplaySwitchLatencyTracker): CoreStartable - /** Inject into NotificationChannels. */ @Binds @IntoMap @@ -353,4 +351,15 @@ abstract class SystemUICoreStartableModule { @IntoMap @ClassKey(ComplicationTypesUpdater::class) abstract fun bindComplicationTypesUpdater(updater: ComplicationTypesUpdater): CoreStartable + + companion object { + @Provides + @IntoMap + @ClassKey(DisplaySwitchLatencyTracker::class) + fun provideDisplaySwitchLatencyTracker( + noCoolDownVariant: Provider<NoCooldownDisplaySwitchLatencyTracker>, + coolDownVariant: Provider<DisplaySwitchLatencyTracker>, + ): CoreStartable = + if (unfoldLatencyTrackingFix()) coolDownVariant.get() else noCoolDownVariant.get() + } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt index 9ce2ce0100a3..7437d3e215d5 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt @@ -19,7 +19,6 @@ package com.android.systemui.dreams.ui.viewmodel import com.android.compose.animation.scene.Swipe import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult -import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel @@ -40,7 +39,6 @@ import kotlinx.coroutines.flow.map class DreamUserActionsViewModel @AssistedInject constructor( - private val communalInteractor: CommunalInteractor, private val deviceUnlockedInteractor: DeviceUnlockedInteractor, private val shadeInteractor: ShadeInteractor, private val shadeModeInteractor: ShadeModeInteractor, @@ -54,14 +52,9 @@ constructor( } else { combine( deviceUnlockedInteractor.deviceUnlockStatus.map { it.isUnlocked }, - communalInteractor.isCommunalAvailable, shadeModeInteractor.shadeMode, - ) { isDeviceUnlocked, isCommunalAvailable, shadeMode -> + ) { isDeviceUnlocked, shadeMode -> buildList { - if (isCommunalAvailable) { - add(Swipe.Start to Scenes.Communal) - } - val bouncerOrGone = if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer add(Swipe.Up to bouncerOrGone) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index 23be5c52ab5c..c61530c3dbcc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -163,10 +163,6 @@ constructor( } private fun bindJankViewModel() { - if (SceneContainerFlag.isEnabled) { - return - } - jankHandle?.dispose() jankHandle = KeyguardJankBinder.bind( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index efa9c21f96b4..caf0fd4450fc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -16,7 +16,6 @@ package com.android.systemui.keyguard; -import static android.app.KeyguardManager.LOCK_ON_USER_SWITCH_CALLBACK; import static android.app.StatusBarManager.SESSION_KEYGUARD; import static android.provider.Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT; import static android.provider.Settings.System.LOCKSCREEN_SOUNDS_ENABLED; @@ -76,7 +75,6 @@ import android.os.Bundle; import android.os.DeadObjectException; import android.os.Handler; import android.os.IBinder; -import android.os.IRemoteCallback; import android.os.Looper; import android.os.Message; import android.os.PowerManager; @@ -194,8 +192,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; -import java.util.Iterator; -import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -286,9 +282,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private static final int SYSTEM_READY = 18; private static final int CANCEL_KEYGUARD_EXIT_ANIM = 19; private static final int BOOT_INTERACTOR = 20; - private static final int BEFORE_USER_SWITCHING = 21; - private static final int USER_SWITCHING = 22; - private static final int USER_SWITCH_COMPLETE = 23; /** Enum for reasons behind updating wakeAndUnlock state. */ @Retention(RetentionPolicy.SOURCE) @@ -306,8 +299,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, int WAKE_AND_UNLOCK = 3; } - private final List<LockNowCallback> mLockNowCallbacks = new ArrayList<>(); - /** * The default amount of time we stay awake (used for all key input) */ @@ -366,18 +357,13 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private final Lazy<NotificationShadeDepthController> mNotificationShadeDepthController; private final Lazy<ShadeController> mShadeController; private final Lazy<CommunalSceneInteractor> mCommunalSceneInteractor; - /* - * Records the user id on request to go away, for validation when WM calls back to start the - * exit animation. - */ - private int mGoingAwayRequestedForUserId = -1; - private boolean mSystemReady; private boolean mBootCompleted; private boolean mBootSendUserPresent; private boolean mShuttingDown; private boolean mDozing; private boolean mAnimatingScreenOff; + private boolean mIgnoreDismiss; private final Context mContext; private final FalsingCollector mFalsingCollector; @@ -640,78 +626,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } }; - @VisibleForTesting - protected UserTracker.Callback mUserChangedCallback = new UserTracker.Callback() { - - @Override - public void onBeforeUserSwitching(int newUser, @NonNull Runnable resultCallback) { - mHandler.sendMessage(mHandler.obtainMessage(BEFORE_USER_SWITCHING, - newUser, 0, resultCallback)); - } - - @Override - public void onUserChanging(int newUser, @NonNull Context userContext, - @NonNull Runnable resultCallback) { - mHandler.sendMessage(mHandler.obtainMessage(USER_SWITCHING, - newUser, 0, resultCallback)); - } - - @Override - public void onUserChanged(int newUser, Context userContext) { - mHandler.sendMessage(mHandler.obtainMessage(USER_SWITCH_COMPLETE, - newUser, 0)); - } - }; - - /** - * Handle {@link #BEFORE_USER_SWITCHING} - */ - @VisibleForTesting - void handleBeforeUserSwitching(int userId, Runnable resultCallback) { - Log.d(TAG, String.format("onBeforeUserSwitching %d", userId)); - synchronized (KeyguardViewMediator.this) { - mHandler.removeMessages(DISMISS); - notifyTrustedChangedLocked(mUpdateMonitor.getUserHasTrust(userId)); - resetKeyguardDonePendingLocked(); - adjustStatusBarLocked(); - mKeyguardStateController.notifyKeyguardGoingAway(false); - if (mLockPatternUtils.isSecure(userId) && !mShowing) { - doKeyguardLocked(null); - } else { - resetStateLocked(); - } - resultCallback.run(); - } - } - - /** - * Handle {@link #USER_SWITCHING} - */ - @VisibleForTesting - void handleUserSwitching(int userId, Runnable resultCallback) { - Log.d(TAG, String.format("onUserSwitching %d", userId)); - synchronized (KeyguardViewMediator.this) { - if (!mLockPatternUtils.isSecure(userId)) { - dismiss(null, null); - } - resultCallback.run(); - } - } - - /** - * Handle {@link #USER_SWITCH_COMPLETE} - */ - @VisibleForTesting - void handleUserSwitchComplete(int userId) { - Log.d(TAG, String.format("onUserSwitchComplete %d", userId)); - // Calling dismiss on a secure user will show the bouncer - if (mLockPatternUtils.isSecure(userId)) { - // We are calling dismiss with a delay as there are race conditions in some scenarios - // caused by async layout listeners - mHandler.postDelayed(() -> dismiss(null /* callback */, null /* message */), 500); - } - } - KeyguardUpdateMonitorCallback mUpdateCallback = new KeyguardUpdateMonitorCallback() { @Override @@ -728,6 +642,27 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } @Override + public void onUserSwitching(int userId) { + Log.d(TAG, String.format("onUserSwitching %d", userId)); + synchronized (KeyguardViewMediator.this) { + mIgnoreDismiss = true; + notifyTrustedChangedLocked(mUpdateMonitor.getUserHasTrust(userId)); + resetKeyguardDonePendingLocked(); + resetStateLocked(); + adjustStatusBarLocked(); + } + } + + @Override + public void onUserSwitchComplete(int userId) { + mIgnoreDismiss = false; + Log.d(TAG, String.format("onUserSwitchComplete %d", userId)); + // We are calling dismiss with a delay as there are race conditions in some scenarios + // caused by async layout listeners + mHandler.postDelayed(() -> dismiss(null /* callback */, null /* message */), 500); + } + + @Override public void onDeviceProvisioned() { sendUserPresentBroadcast(); } @@ -1736,13 +1671,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, com.android.internal.R.anim.lock_screen_behind_enter); mWorkLockController = new WorkLockActivityController(mContext, mUserTracker); - mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor()); - // start() can be invoked in the middle of user switching, so check for this state and issue - // the call manually as that important event was missed. - if (mUserTracker.isUserSwitching()) { - handleBeforeUserSwitching(mUserTracker.getUserId(), () -> {}); - handleUserSwitching(mUserTracker.getUserId(), () -> {}); - } + mJavaAdapter.alwaysCollectFlow( mWallpaperRepository.getWallpaperSupportsAmbientMode(), this::setWallpaperSupportsAmbientMode); @@ -1791,7 +1720,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // System ready can be invoked in the middle of user switching, so check for this state // and issue the call manually as that important event was missed. if (mUserTracker.isUserSwitching()) { - mUserChangedCallback.onUserChanging(mUserTracker.getUserId(), mContext, () -> {}); + mUpdateCallback.onUserSwitching(mUserTracker.getUserId()); } } // Most services aren't available until the system reaches the ready state, so we @@ -2432,23 +2361,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mCommunalSceneInteractor.get().showHubFromPowerButton(); } - int currentUserId = mSelectedUserInteractor.getSelectedUserId(); - if (options != null && options.getBinder(LOCK_ON_USER_SWITCH_CALLBACK) != null) { - LockNowCallback callback = new LockNowCallback(currentUserId, - IRemoteCallback.Stub.asInterface( - options.getBinder(LOCK_ON_USER_SWITCH_CALLBACK))); - synchronized (mLockNowCallbacks) { - mLockNowCallbacks.add(callback); - } - Log.d(TAG, "LockNowCallback required for user: " + callback.mUserId); - } - // if another app is disabling us, don't show if (!mExternallyEnabled && !mLockPatternUtils.isUserInLockdown( mSelectedUserInteractor.getSelectedUserId())) { if (DEBUG) Log.d(TAG, "doKeyguard: not showing because externally disabled"); - notifyLockNowCallback(); + mNeedToReshowWhenReenabled = true; return; } @@ -2466,7 +2384,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // We're removing "reset" in the refactor - "resetting" the views will happen // as a reaction to the root cause of the "reset" signal. if (KeyguardWmStateRefactor.isEnabled()) { - notifyLockNowCallback(); return; } @@ -2479,7 +2396,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, + "previously hiding. It should be safe to short-circuit " + "here."); resetStateLocked(/* hideBouncer= */ false); - notifyLockNowCallback(); return; } } else { @@ -2506,7 +2422,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Log.d(TAG, "doKeyguard: not showing because device isn't provisioned and the sim is" + " not locked or missing"); } - notifyLockNowCallback(); return; } @@ -2514,7 +2429,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, if (mLockPatternUtils.isLockScreenDisabled(mSelectedUserInteractor.getSelectedUserId()) && !lockedOrMissing && !forceShow) { if (DEBUG) Log.d(TAG, "doKeyguard: not showing because lockscreen is off"); - notifyLockNowCallback(); return; } @@ -2562,6 +2476,11 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } public void dismiss(IKeyguardDismissCallback callback, CharSequence message) { + if (mIgnoreDismiss) { + android.util.Log.i(TAG, "Ignoring request to dismiss (user switch in progress?)"); + return; + } + if (mKeyguardStateController.isKeyguardGoingAway()) { Log.i(TAG, "Ignoring dismiss because we're already going away."); return; @@ -2579,7 +2498,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } private void resetStateLocked(boolean hideBouncer) { - if (DEBUG) Log.d(TAG, "resetStateLocked"); + if (DEBUG) Log.e(TAG, "resetStateLocked"); Message msg = mHandler.obtainMessage(RESET, hideBouncer ? 1 : 0, 0); mHandler.sendMessage(msg); } @@ -2827,18 +2746,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, message = "BOOT_INTERACTOR"; handleBootInteractor(); break; - case BEFORE_USER_SWITCHING: - message = "BEFORE_USER_SWITCHING"; - handleBeforeUserSwitching(msg.arg1, (Runnable) msg.obj); - break; - case USER_SWITCHING: - message = "USER_SWITCHING"; - handleUserSwitching(msg.arg1, (Runnable) msg.obj); - break; - case USER_SWITCH_COMPLETE: - message = "USER_SWITCH_COMPLETE"; - handleUserSwitchComplete(msg.arg1); - break; } Log.d(TAG, "KeyguardViewMediator queue processing message: " + message); } @@ -2980,9 +2887,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mUiBgExecutor.execute(() -> { Log.d(TAG, "updateActivityLockScreenState(" + showing + ", " + aodShowing + ", " + reason + ")"); - if (showing) { - notifyLockNowCallback(); - } if (KeyguardWmStateRefactor.isEnabled()) { // Handled in WmLockscreenVisibilityManager if flag is enabled. @@ -3027,7 +2931,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, synchronized (KeyguardViewMediator.this) { if (!mSystemReady) { if (DEBUG) Log.d(TAG, "ignoring handleShow because system is not ready."); - notifyLockNowCallback(); return; } if (DEBUG) Log.d(TAG, "handleShow"); @@ -3086,11 +2989,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } } - final Runnable mKeyguardGoingAwayRunnable = new Runnable() { + private final Runnable mKeyguardGoingAwayRunnable = new Runnable() { @SuppressLint("MissingPermission") @Override public void run() { Trace.beginSection("KeyguardViewMediator.mKeyGuardGoingAwayRunnable"); + Log.d(TAG, "keyguardGoingAwayRunnable"); mKeyguardViewControllerLazy.get().keyguardGoingAway(); int flags = 0; @@ -3127,10 +3031,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // Handled in WmLockscreenVisibilityManager if flag is enabled. if (!KeyguardWmStateRefactor.isEnabled()) { - mGoingAwayRequestedForUserId = mSelectedUserInteractor.getSelectedUserId(); - Log.d(TAG, "keyguardGoingAway requested for userId: " - + mGoingAwayRequestedForUserId); - // Don't actually hide the Keyguard at the moment, wait for window manager // until it tells us it's safe to do so with startKeyguardExitAnimation. // Posting to mUiOffloadThread to ensure that calls to ActivityTaskManager @@ -3269,30 +3169,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, RemoteAnimationTarget[] nonApps, IRemoteAnimationFinishedCallback finishedCallback) { Log.d(TAG, "handleStartKeyguardExitAnimation startTime=" + startTime + " fadeoutDuration=" + fadeoutDuration); - int currentUserId = mSelectedUserInteractor.getSelectedUserId(); - if (!KeyguardWmStateRefactor.isEnabled() && mGoingAwayRequestedForUserId != currentUserId) { - Log.e(TAG, "Not executing handleStartKeyguardExitAnimationInner() due to userId " - + "mismatch. Requested: " + mGoingAwayRequestedForUserId + ", current: " - + currentUserId); - if (finishedCallback != null) { - // There will not execute animation, send a finish callback to ensure the remote - // animation won't hang there. - try { - finishedCallback.onAnimationFinished(); - } catch (RemoteException e) { - Slog.w(TAG, "Failed to call onAnimationFinished", e); - } - } - mHiding = false; - if (mLockPatternUtils.isSecure(currentUserId)) { - doKeyguardLocked(null); - } else { - resetStateLocked(); - dismiss(null, null); - } - return; - } - synchronized (KeyguardViewMediator.this) { mIsKeyguardExitAnimationCanceled = false; // Tell ActivityManager that we canceled the keyguard animation if @@ -3537,13 +3413,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, * app transition before finishing the current RemoteAnimation, or the keyguard being re-shown). */ private void handleCancelKeyguardExitAnimation() { - if (!KeyguardWmStateRefactor.isEnabled() - && mGoingAwayRequestedForUserId != mSelectedUserInteractor.getSelectedUserId()) { - Log.e(TAG, "Setting pendingLock = true due to userId mismatch. Requested: " - + mGoingAwayRequestedForUserId + ", current: " - + mSelectedUserInteractor.getSelectedUserId()); - setPendingLock(true); - } if (mPendingLock) { Log.d(TAG, "#handleCancelKeyguardExitAnimation: keyguard exit animation cancelled. " + "There's a pending lock, so we were cancelled because the device was locked " @@ -3644,7 +3513,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mSurfaceBehindRemoteAnimationRequested = true; if (ENABLE_NEW_KEYGUARD_SHELL_TRANSITIONS && !KeyguardWmStateRefactor.isEnabled()) { - mGoingAwayRequestedForUserId = mSelectedUserInteractor.getSelectedUserId(); startKeyguardTransition(false /* keyguardShowing */, false /* aodShowing */); return; } @@ -3665,9 +3533,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, if (!KeyguardWmStateRefactor.isEnabled()) { // Handled in WmLockscreenVisibilityManager. - mGoingAwayRequestedForUserId = mSelectedUserInteractor.getSelectedUserId(); - Log.d(TAG, "keyguardGoingAway requested for userId: " - + mGoingAwayRequestedForUserId); mActivityTaskManagerService.keyguardGoingAway(flags); } } catch (RemoteException e) { @@ -4123,29 +3988,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mUiBgExecutor.execute(mTrustManager::reportKeyguardShowingChanged); } - private void notifyLockNowCallback() { - List<LockNowCallback> callbacks; - synchronized (mLockNowCallbacks) { - callbacks = new ArrayList<LockNowCallback>(mLockNowCallbacks); - mLockNowCallbacks.clear(); - } - Iterator<LockNowCallback> iter = callbacks.listIterator(); - while (iter.hasNext()) { - LockNowCallback callback = iter.next(); - iter.remove(); - if (callback.mUserId != mSelectedUserInteractor.getSelectedUserId()) { - Log.i(TAG, "Not notifying lockNowCallback due to user mismatch"); - continue; - } - Log.i(TAG, "Notifying lockNowCallback"); - try { - callback.mRemoteCallback.sendResult(null); - } catch (RemoteException e) { - Log.e(TAG, "Could not issue LockNowCallback sendResult", e); - } - } - } - private void notifyTrustedChangedLocked(boolean trusted) { int size = mKeyguardStateCallbacks.size(); for (int i = size - 1; i >= 0; i--) { @@ -4310,14 +4152,4 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } }; } - - private class LockNowCallback { - final int mUserId; - final IRemoteCallback mRemoteCallback; - - LockNowCallback(int userId, IRemoteCallback remoteCallback) { - mUserId = userId; - mRemoteCallback = remoteCallback; - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt index f85a23c1f091..eb96c921c181 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt @@ -24,6 +24,7 @@ import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.Flags.communalSceneKtfRefactor import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor +import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -57,6 +58,7 @@ constructor( keyguardInteractor: KeyguardInteractor, powerInteractor: PowerInteractor, private val communalInteractor: CommunalInteractor, + private val communalSettingsInteractor: CommunalSettingsInteractor, private val communalSceneInteractor: CommunalSceneInteractor, keyguardOcclusionInteractor: KeyguardOcclusionInteractor, val deviceEntryInteractor: DeviceEntryInteractor, @@ -116,6 +118,17 @@ constructor( } } + @SuppressLint("MissingPermission") + private fun shouldTransitionToCommunal( + shouldShowCommunal: Boolean, + isCommunalAvailable: Boolean, + ) = + if (communalSettingsInteractor.isV2FlagEnabled()) { + shouldShowCommunal + } else { + isCommunalAvailable && dreamManager.canStartDreaming(false) + } + @OptIn(FlowPreview::class) @SuppressLint("MissingPermission") private fun listenForDozingToDreaming() { @@ -141,9 +154,10 @@ constructor( .filterRelevantKeyguardStateAnd { isAwake -> isAwake } .sample( communalInteractor.isCommunalAvailable, + communalInteractor.shouldShowCommunal, communalSceneInteractor.isIdleOnCommunal, ) - .collect { (_, isCommunalAvailable, isIdleOnCommunal) -> + .collect { (_, isCommunalAvailable, shouldShowCommunal, isIdleOnCommunal) -> val isKeyguardOccludedLegacy = keyguardInteractor.isKeyguardOccluded.value val primaryBouncerShowing = keyguardInteractor.primaryBouncerShowing.value val isKeyguardGoingAway = keyguardInteractor.isKeyguardGoingAway.value @@ -177,11 +191,9 @@ constructor( if (!SceneContainerFlag.isEnabled) { startTransitionTo(KeyguardState.GLANCEABLE_HUB) } - } else if (isCommunalAvailable && dreamManager.canStartDreaming(false)) { - // Using false for isScreenOn as canStartDreaming returns false if any - // dream, including doze, is active. - // This case handles tapping the power button to transition through - // dream -> off -> hub. + } else if ( + shouldTransitionToCommunal(shouldShowCommunal, isCommunalAvailable) + ) { if (!SceneContainerFlag.isEnabled) { transitionToGlanceableHub() } @@ -203,6 +215,7 @@ constructor( powerInteractor.detailedWakefulness .filterRelevantKeyguardStateAnd { it.isAwake() } .sample( + communalInteractor.shouldShowCommunal, communalInteractor.isCommunalAvailable, communalSceneInteractor.isIdleOnCommunal, keyguardInteractor.biometricUnlockState, @@ -212,6 +225,7 @@ constructor( .collect { ( _, + shouldShowCommunal, isCommunalAvailable, isIdleOnCommunal, biometricUnlockState, @@ -245,7 +259,9 @@ constructor( ownerReason = "waking from dozing", ) } - } else if (isCommunalAvailable && dreamManager.canStartDreaming(true)) { + } else if ( + shouldTransitionToCommunal(shouldShowCommunal, isCommunalAvailable) + ) { if (!SceneContainerFlag.isEnabled) { transitionToGlanceableHub() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt index 251af11f7fe6..c1c509b8fd57 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt @@ -129,20 +129,37 @@ constructor( if (!communalSettingsInteractor.isCommunalFlagEnabled()) return if (SceneContainerFlag.isEnabled) return scope.launch { - powerInteractor.isAwake - .debounce(50L) - .filterRelevantKeyguardStateAnd { isAwake -> isAwake } - .sample(communalInteractor.isCommunalAvailable) - .collect { isCommunalAvailable -> - if (isCommunalAvailable && dreamManager.canStartDreaming(false)) { - // This case handles tapping the power button to transition through - // dream -> off -> hub. - communalSceneInteractor.snapToScene( - newScene = CommunalScenes.Communal, - loggingReason = "from dreaming to hub", - ) + if (communalSettingsInteractor.isV2FlagEnabled()) { + powerInteractor.isAwake + .debounce(50L) + .filterRelevantKeyguardStateAnd { isAwake -> isAwake } + .sample(communalInteractor.shouldShowCommunal) + .collect { shouldShowCommunal -> + if (shouldShowCommunal) { + // This case handles tapping the power button to transition through + // dream -> off -> hub. + communalSceneInteractor.snapToScene( + newScene = CommunalScenes.Communal, + loggingReason = "from dreaming to hub", + ) + } } - } + } else { + powerInteractor.isAwake + .debounce(50L) + .filterRelevantKeyguardStateAnd { isAwake -> isAwake } + .sample(communalInteractor.isCommunalAvailable) + .collect { isCommunalAvailable -> + if (isCommunalAvailable && dreamManager.canStartDreaming(false)) { + // This case handles tapping the power button to transition through + // dream -> off -> hub. + communalSceneInteractor.snapToScene( + newScene = CommunalScenes.Communal, + loggingReason = "from dreaming to hub", + ) + } + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt index 382436cf9397..5f821022d580 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt @@ -215,6 +215,7 @@ constructor( animator = null, modeOnCanceled = TransitionModeOnCanceled.RESET, ) + repository.nextLockscreenTargetState.value = DEFAULT_STATE startTransition(newTransition) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt index 5c03d65e570f..8f6815829ba2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt @@ -69,7 +69,7 @@ constructor( * Note that [onCancel] isn't used when the scene framework is enabled. */ fun sharedFlow( - duration: Duration, + duration: Duration = transitionDuration, onStep: (Float) -> Float, startTime: Duration = 0.milliseconds, onStart: (() -> Unit)? = null, 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 index 0cb684a1aabe..38263be33c82 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardJankBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardJankBinder.kt @@ -30,6 +30,7 @@ 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 com.android.systemui.scene.shared.flag.SceneContainerFlag import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -79,15 +80,18 @@ object KeyguardJankBinder { } } - launch { - viewModel.lockscreenToAodTransition.collect { - processStep(it, CUJ_LOCKSCREEN_TRANSITION_TO_AOD) + // The following is already done in KeyguardTransitionAnimationCallbackImpl. + if (!SceneContainerFlag.isEnabled) { + launch { + viewModel.lockscreenToAodTransition.collect { + processStep(it, CUJ_LOCKSCREEN_TRANSITION_TO_AOD) + } } - } - launch { - viewModel.aodToLockscreenTransition.collect { - processStep(it, CUJ_LOCKSCREEN_TRANSITION_FROM_AOD) + launch { + viewModel.aodToLockscreenTransition.collect { + processStep(it, CUJ_LOCKSCREEN_TRANSITION_FROM_AOD) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/GlanceableHubBlurProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/GlanceableHubBlurProvider.kt index 19cd501fa787..50f8e086ac6e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/GlanceableHubBlurProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/GlanceableHubBlurProvider.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.transitions +import android.util.MathUtils import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -33,8 +34,18 @@ constructor( blurConfig: BlurConfig, ) { val exitBlurRadius: Flow<Float> = - transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx) + transitionAnimation.sharedFlow( + onStep = { MathUtils.lerp(blurConfig.maxBlurRadiusPx, blurConfig.minBlurRadiusPx, it) }, + onStart = { blurConfig.maxBlurRadiusPx }, + onFinish = { blurConfig.minBlurRadiusPx }, + onCancel = { blurConfig.maxBlurRadiusPx }, + ) val enterBlurRadius: Flow<Float> = - transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx) + transitionAnimation.sharedFlow( + onStep = { MathUtils.lerp(blurConfig.minBlurRadiusPx, blurConfig.maxBlurRadiusPx, it) }, + onStart = { blurConfig.minBlurRadiusPx }, + onFinish = { blurConfig.maxBlurRadiusPx }, + onCancel = { blurConfig.minBlurRadiusPx }, + ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt index 3353983ab5a5..06c27d31cc3b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt @@ -19,7 +19,6 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.compose.animation.scene.Swipe import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult -import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor import com.android.systemui.scene.shared.model.Scenes @@ -43,7 +42,6 @@ class LockscreenUserActionsViewModel @AssistedInject constructor( private val deviceEntryInteractor: DeviceEntryInteractor, - private val communalInteractor: CommunalInteractor, private val shadeInteractor: ShadeInteractor, private val shadeModeInteractor: ShadeModeInteractor, private val occlusionInteractor: SceneContainerOcclusionInteractor, @@ -58,15 +56,10 @@ constructor( combine( deviceEntryInteractor.isUnlocked, - communalInteractor.isCommunalAvailable, shadeModeInteractor.shadeMode, occlusionInteractor.isOccludingActivityShown, - ) { isDeviceUnlocked, isCommunalAvailable, shadeMode, isOccluded -> + ) { isDeviceUnlocked, shadeMode, isOccluded -> buildList { - if (isCommunalAvailable) { - add(Swipe.Start to Scenes.Communal) - } - add(Swipe.Up to if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer) addAll( diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java index 912ace7675d5..e5eec64ac615 100644 --- a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java +++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java @@ -27,6 +27,7 @@ import android.content.pm.PackageManager; import androidx.annotation.Nullable; import com.android.dream.lowlight.LowLightDreamManager; +import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.SystemUser; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.shared.condition.Condition; @@ -36,6 +37,7 @@ import com.android.systemui.util.condition.ConditionalCoreStartable; import dagger.Lazy; import java.util.Set; +import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; @@ -59,6 +61,8 @@ public class LowLightMonitor extends ConditionalCoreStartable implements Monitor private final PackageManager mPackageManager; + private final Executor mExecutor; + @Inject public LowLightMonitor(Lazy<LowLightDreamManager> lowLightDreamManager, @SystemUser Monitor conditionsMonitor, @@ -66,7 +70,8 @@ public class LowLightMonitor extends ConditionalCoreStartable implements Monitor ScreenLifecycle screenLifecycle, LowLightLogger lowLightLogger, @Nullable @Named(LOW_LIGHT_DREAM_SERVICE) ComponentName lowLightDreamService, - PackageManager packageManager) { + PackageManager packageManager, + @Background Executor backgroundExecutor) { super(conditionsMonitor); mLowLightDreamManager = lowLightDreamManager; mConditionsMonitor = conditionsMonitor; @@ -75,59 +80,69 @@ public class LowLightMonitor extends ConditionalCoreStartable implements Monitor mLogger = lowLightLogger; mLowLightDreamService = lowLightDreamService; mPackageManager = packageManager; + mExecutor = backgroundExecutor; } @Override public void onConditionsChanged(boolean allConditionsMet) { - mLogger.d(TAG, "Low light enabled: " + allConditionsMet); + mExecutor.execute(() -> { + mLogger.d(TAG, "Low light enabled: " + allConditionsMet); - mLowLightDreamManager.get().setAmbientLightMode(allConditionsMet - ? AMBIENT_LIGHT_MODE_LOW_LIGHT : AMBIENT_LIGHT_MODE_REGULAR); + mLowLightDreamManager.get().setAmbientLightMode(allConditionsMet + ? AMBIENT_LIGHT_MODE_LOW_LIGHT : AMBIENT_LIGHT_MODE_REGULAR); + }); } @Override public void onScreenTurnedOn() { - if (mSubscriptionToken == null) { - mLogger.d(TAG, "Screen turned on. Subscribing to low light conditions."); - - mSubscriptionToken = mConditionsMonitor.addSubscription( - new Monitor.Subscription.Builder(this) - .addConditions(mLowLightConditions.get()) - .build()); - } + mExecutor.execute(() -> { + if (mSubscriptionToken == null) { + mLogger.d(TAG, "Screen turned on. Subscribing to low light conditions."); + + mSubscriptionToken = mConditionsMonitor.addSubscription( + new Monitor.Subscription.Builder(this) + .addConditions(mLowLightConditions.get()) + .build()); + } + }); } @Override public void onScreenTurnedOff() { - if (mSubscriptionToken != null) { - mLogger.d(TAG, "Screen turned off. Removing subscription to low light conditions."); - - mConditionsMonitor.removeSubscription(mSubscriptionToken); - mSubscriptionToken = null; - } + mExecutor.execute(() -> { + if (mSubscriptionToken != null) { + mLogger.d(TAG, "Screen turned off. Removing subscription to low light conditions."); + + mConditionsMonitor.removeSubscription(mSubscriptionToken); + mSubscriptionToken = null; + } + }); } @Override protected void onStart() { - if (mLowLightDreamService != null) { - // Note that the dream service is disabled by default. This prevents the dream from - // appearing in settings on devices that don't have it explicitly excluded (done in - // the settings overlay). Therefore, the component is enabled if it is to be used - // here. - mPackageManager.setComponentEnabledSetting( - mLowLightDreamService, - PackageManager.COMPONENT_ENABLED_STATE_ENABLED, - PackageManager.DONT_KILL_APP - ); - } else { - // If there is no low light dream service, do not observe conditions. - return; - } - - mScreenLifecycle.addObserver(this); - if (mScreenLifecycle.getScreenState() == SCREEN_ON) { - onScreenTurnedOn(); - } + mExecutor.execute(() -> { + if (mLowLightDreamService != null) { + // Note that the dream service is disabled by default. This prevents the dream from + // appearing in settings on devices that don't have it explicitly excluded (done in + // the settings overlay). Therefore, the component is enabled if it is to be used + // here. + mPackageManager.setComponentEnabledSetting( + mLowLightDreamService, + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + PackageManager.DONT_KILL_APP + ); + } else { + // If there is no low light dream service, do not observe conditions. + return; + } + + mScreenLifecycle.addObserver(this); + if (mScreenLifecycle.getScreenState() == SCREEN_ON) { + onScreenTurnedOn(); + } + }); + } } diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java index 8469cb4ab565..f8072f2f79b4 100644 --- a/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java +++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java @@ -78,7 +78,7 @@ public abstract class LowLightModule { @Provides @IntoSet - @Named(com.android.systemui.lowlightclock.dagger.LowLightModule.LOW_LIGHT_PRECONDITIONS) + @Named(LOW_LIGHT_PRECONDITIONS) static Condition provideLowLightCondition(LowLightCondition lowLightCondition, DirectBootCondition directBootCondition) { // Start lowlight if we are either in lowlight or in direct boot. The ordering of the diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt index d63c2e07b94f..0107a5278e3e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt @@ -23,11 +23,11 @@ import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.ViewOutlineProvider +import androidx.annotation.VisibleForTesting import androidx.core.view.GestureDetectorCompat import androidx.dynamicanimation.animation.FloatPropertyCompat import androidx.dynamicanimation.animation.SpringForce import com.android.app.tracing.TraceStateLogger -import com.android.internal.annotations.VisibleForTesting import com.android.settingslib.Utils import com.android.systemui.Gefingerpoken import com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS @@ -38,9 +38,10 @@ import com.android.systemui.res.R import com.android.systemui.util.animation.TransitionLayout import com.android.systemui.util.concurrency.DelayableExecutor import com.android.wm.shell.shared.animation.PhysicsAnimator +import kotlin.math.sign private const val FLING_SLOP = 1000000 -private const val DISMISS_DELAY = 100L +@VisibleForTesting const val DISMISS_DELAY = 100L private const val SCROLL_DELAY = 100L private const val RUBBERBAND_FACTOR = 0.2f private const val SETTINGS_BUTTON_TRANSLATION_FRACTION = 0.3f @@ -64,7 +65,7 @@ class MediaCarouselScrollHandler( private val closeGuts: (immediate: Boolean) -> Unit, private val falsingManager: FalsingManager, private val logSmartspaceImpression: (Boolean) -> Unit, - private val logger: MediaUiEventLogger + private val logger: MediaUiEventLogger, ) { /** Trace state logger for media carousel visibility */ private val visibleStateLogger = TraceStateLogger("$TAG#visibleToUser") @@ -96,7 +97,7 @@ class MediaCarouselScrollHandler( /** What's the currently visible player index? */ var visibleMediaIndex: Int = 0 - private set + @VisibleForTesting set /** How much are we scrolled into the current media? */ private var scrollIntoCurrentMedia: Int = 0 @@ -137,14 +138,14 @@ class MediaCarouselScrollHandler( eStart: MotionEvent?, eCurrent: MotionEvent, vX: Float, - vY: Float + vY: Float, ) = onFling(vX, vY) override fun onScroll( down: MotionEvent?, lastMotion: MotionEvent, distanceX: Float, - distanceY: Float + distanceY: Float, ) = onScroll(down!!, lastMotion, distanceX) override fun onDown(e: MotionEvent): Boolean { @@ -157,6 +158,7 @@ class MediaCarouselScrollHandler( val touchListener = object : Gefingerpoken { override fun onTouchEvent(motionEvent: MotionEvent?) = onTouch(motionEvent!!) + override fun onInterceptTouchEvent(ev: MotionEvent?) = onInterceptTouch(ev!!) } @@ -168,7 +170,7 @@ class MediaCarouselScrollHandler( scrollX: Int, scrollY: Int, oldScrollX: Int, - oldScrollY: Int + oldScrollY: Int, ) { if (playerWidthPlusPadding == 0) { return @@ -177,7 +179,7 @@ class MediaCarouselScrollHandler( val relativeScrollX = scrollView.relativeScrollX onMediaScrollingChanged( relativeScrollX / playerWidthPlusPadding, - relativeScrollX % playerWidthPlusPadding + relativeScrollX % playerWidthPlusPadding, ) } } @@ -209,7 +211,7 @@ class MediaCarouselScrollHandler( 0, carouselWidth, carouselHeight, - cornerRadius.toFloat() + cornerRadius.toFloat(), ) } } @@ -235,7 +237,7 @@ class MediaCarouselScrollHandler( getMaxTranslation().toFloat(), 0.0f, 1.0f, - Math.abs(contentTranslation) + Math.abs(contentTranslation), ) val settingsTranslation = (1.0f - settingsOffset) * @@ -323,7 +325,7 @@ class MediaCarouselScrollHandler( CONTENT_TRANSLATION, newTranslation, startVelocity = 0.0f, - config = translationConfig + config = translationConfig, ) .start() scrollView.animationTargetX = newTranslation @@ -391,7 +393,7 @@ class MediaCarouselScrollHandler( CONTENT_TRANSLATION, newTranslation, startVelocity = 0.0f, - config = translationConfig + config = translationConfig, ) .start() } else { @@ -430,7 +432,7 @@ class MediaCarouselScrollHandler( CONTENT_TRANSLATION, newTranslation, startVelocity = vX, - config = translationConfig + config = translationConfig, ) .start() scrollView.animationTargetX = newTranslation @@ -583,10 +585,35 @@ class MediaCarouselScrollHandler( // We need to post this to wait for the active player becomes visible. mainExecutor.executeDelayed( { scrollView.smoothScrollTo(view.left, scrollView.scrollY) }, - SCROLL_DELAY + SCROLL_DELAY, ) } + /** + * Scrolls the media carousel by the number of players specified by [step]. If scrolling beyond + * the carousel's bounds: + * - If the carousel is not dismissible, the settings button is displayed. + * - If the carousel is dismissible, no action taken. + * + * @param step A positive number means next, and negative means previous. + */ + fun scrollByStep(step: Int) { + val destIndex = visibleMediaIndex + step + if (destIndex >= mediaContent.childCount || destIndex < 0) { + if (!showsSettingsButton) return + var translation = getMaxTranslation() * sign(-step.toFloat()) + translation = if (isRtl) -translation else translation + PhysicsAnimator.getInstance(this) + .spring(CONTENT_TRANSLATION, translation, config = translationConfig) + .start() + scrollView.animationTargetX = translation + } else if (scrollView.getContentTranslation() != 0.0f) { + resetTranslation(true) + } else { + scrollToPlayer(destIndex = destIndex) + } + } + companion object { private val CONTENT_TRANSLATION = object : FloatPropertyCompat<MediaCarouselScrollHandler>("contentTranslation") { diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java index c9740811101b..be814aecc42b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java @@ -58,7 +58,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { private static final float DEVICE_DISABLED_ALPHA = 0.5f; private static final float DEVICE_ACTIVE_ALPHA = 1f; protected List<MediaItem> mMediaItemList = new CopyOnWriteArrayList<>(); - private boolean mShouldGroupSelectedMediaItems = Flags.enableOutputSwitcherSessionGrouping(); + private boolean mShouldGroupSelectedMediaItems = Flags.enableOutputSwitcherDeviceGrouping(); public MediaOutputAdapter(MediaSwitchingController controller) { super(controller); @@ -333,7 +333,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { } private boolean shouldShowGroupCheckbox(@NonNull GroupStatus groupStatus) { - if (Flags.enableOutputSwitcherSessionGrouping()) { + if (Flags.enableOutputSwitcherDeviceGrouping()) { return isGroupCheckboxEnabled(groupStatus); } return true; @@ -391,7 +391,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { if (drawable instanceof AnimatedVectorDrawable) { ((AnimatedVectorDrawable) drawable).start(); } - if (Flags.enableOutputSwitcherSessionGrouping()) { + if (Flags.enableOutputSwitcherDeviceGrouping()) { mEndClickIcon.setContentDescription(mContext.getString(accessibilityStringId)); } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java index 51437b3bbdaf..9d375809786a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java @@ -733,7 +733,7 @@ public class MediaSwitchingController selectedDevicesIds.add(connectedMediaDevice.getId()); } boolean groupSelectedDevices = - com.android.media.flags.Flags.enableOutputSwitcherSessionGrouping(); + com.android.media.flags.Flags.enableOutputSwitcherDeviceGrouping(); int nextSelectedItemIndex = 0; boolean suggestedDeviceAdded = false; boolean displayGroupAdded = false; diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt index 1b9251061f3d..9319961f5b68 100644 --- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt @@ -24,7 +24,7 @@ import com.android.compose.animation.scene.UserActionResult.HideOverlay import com.android.compose.animation.scene.UserActionResult.ShowOverlay import com.android.compose.animation.scene.UserActionResult.ShowOverlay.HideCurrentOverlays import com.android.systemui.scene.shared.model.Overlays -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -38,7 +38,7 @@ class NotificationsShadeOverlayActionsViewModel @AssistedInject constructor() : mapOf( Swipe.Up to HideOverlay(Overlays.NotificationsShade), Back to HideOverlay(Overlays.NotificationsShade), - Swipe.Down(fromSource = SceneContainerEdge.TopRight) to + Swipe.Down(fromSource = SceneContainerArea.EndHalf) to ShowOverlay( Overlays.QuickSettingsShade, hideCurrentOverlays = HideCurrentOverlays.Some(Overlays.NotificationsShade), diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt index 16dff7d11002..11b014c2147f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt @@ -28,6 +28,7 @@ import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.qs.panels.shared.model.PanelsLog import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.TilesUpgradePath import com.android.systemui.settings.UserFileManager import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.util.kotlin.SharedPreferencesExt.observe @@ -83,34 +84,78 @@ constructor( .flowOn(backgroundDispatcher) /** Sets for the current user the set of [TileSpec] to display as large tiles. */ - fun setLargeTilesSpecs(specs: Set<TileSpec>) { - setLargeTilesSpecsForUser(specs, userRepository.getSelectedUserInfo().id) + fun writeLargeTileSpecs(specs: Set<TileSpec>) { + with(getSharedPrefs(userRepository.getSelectedUserInfo().id)) { + writeLargeTileSpecs(specs) + setLargeTilesDefault(false) + } } - private fun setLargeTilesSpecsForUser(specs: Set<TileSpec>, userId: Int) { - with(getSharedPrefs(userId)) { - edit().putStringSet(LARGE_TILES_SPECS_KEY, specs.map { it.spec }.toSet()).apply() + suspend fun deleteLargeTileDataJob() { + userRepository.selectedUserInfo.collect { userInfo -> + getSharedPrefs(userInfo.id) + .edit() + .remove(LARGE_TILES_SPECS_KEY) + .remove(LARGE_TILES_DEFAULT_KEY) + .apply() } } + private fun SharedPreferences.writeLargeTileSpecs(specs: Set<TileSpec>) { + edit().putStringSet(LARGE_TILES_SPECS_KEY, specs.map { it.spec }.toSet()).apply() + } + /** - * Sets the initial tiles as large, if there is no set in SharedPrefs for the [userId]. This is - * to be used when upgrading to a build that supports large/small tiles. + * Sets the initial set of large tiles. One of the following cases will happen: + * * If we are setting the default set (no value stored in settings for the list of tiles), set + * the large tiles based on [defaultLargeTilesRepository]. We do this to signal future reboots + * that we have performed the upgrade path once. In this case, we will mark that we set them + * as the default in case a restore needs to modify them later. + * * If we got a list of tiles restored from a device and nothing has modified the list of + * tiles, set all the restored tiles to large. Note that if we also restored a set of large + * tiles before this was called, [LARGE_TILES_DEFAULT_KEY] will be false and we won't + * overwrite it. + * * If we got a list of tiles from settings, we consider that we upgraded in place and then we + * will set all those tiles to large IF there's no current set of large tiles. * * Even if largeTilesSpec is read Eagerly before we know if we are in an initial state, because * we are not writing the default values to the SharedPreferences, the file will not contain the * key and this call will succeed, as long as there hasn't been any calls to setLargeTilesSpecs * for that user before. */ - fun setInitialLargeTilesSpecs(specs: Set<TileSpec>, userId: Int) { + fun setInitialOrUpgradeLargeTiles(upgradePath: TilesUpgradePath, userId: Int) { with(getSharedPrefs(userId)) { - if (!contains(LARGE_TILES_SPECS_KEY)) { - logger.i("Setting upgraded large tiles for user $userId: $specs") - setLargeTilesSpecsForUser(specs, userId) + when (upgradePath) { + is TilesUpgradePath.DefaultSet -> { + writeLargeTileSpecs(defaultLargeTilesRepository.defaultLargeTiles) + logger.i("Large tiles set to default on init") + setLargeTilesDefault(true) + } + is TilesUpgradePath.RestoreFromBackup -> { + if ( + getBoolean(LARGE_TILES_DEFAULT_KEY, false) || + !contains(LARGE_TILES_SPECS_KEY) + ) { + writeLargeTileSpecs(upgradePath.value) + logger.i("Tiles restored from backup set to large: ${upgradePath.value}") + setLargeTilesDefault(false) + } + } + is TilesUpgradePath.ReadFromSettings -> { + if (!contains(LARGE_TILES_SPECS_KEY)) { + writeLargeTileSpecs(upgradePath.value) + logger.i("Tiles read from settings set to large: ${upgradePath.value}") + setLargeTilesDefault(false) + } + } } } } + private fun SharedPreferences.setLargeTilesDefault(value: Boolean) { + edit().putBoolean(LARGE_TILES_DEFAULT_KEY, value).apply() + } + private fun getSharedPrefs(userId: Int): SharedPreferences { return userFileManager.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE, userId) } @@ -118,6 +163,7 @@ constructor( companion object { private const val TAG = "QSPreferencesRepository" private const val LARGE_TILES_SPECS_KEY = "large_tiles_specs" + private const val LARGE_TILES_DEFAULT_KEY = "large_tiles_default" const val FILE_NAME = "quick_settings_prefs" } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/QSPreferencesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/QSPreferencesInteractor.kt index 86838b438bc6..9b98797ef393 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/QSPreferencesInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/QSPreferencesInteractor.kt @@ -19,6 +19,7 @@ package com.android.systemui.qs.panels.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.qs.panels.data.repository.QSPreferencesRepository import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.TilesUpgradePath import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -27,10 +28,20 @@ class QSPreferencesInteractor @Inject constructor(private val repo: QSPreference val largeTilesSpecs: Flow<Set<TileSpec>> = repo.largeTilesSpecs fun setLargeTilesSpecs(specs: Set<TileSpec>) { - repo.setLargeTilesSpecs(specs) + repo.writeLargeTileSpecs(specs) } - fun setInitialLargeTilesSpecs(specs: Set<TileSpec>, user: Int) { - repo.setInitialLargeTilesSpecs(specs, user) + /** + * This method should be called to indicate that a "new" set of tiles has been determined for a + * particular user coming from different upgrade sources. + * + * @see TilesUpgradePath for more information + */ + fun setInitialOrUpgradeLargeTilesSpecs(specs: TilesUpgradePath, user: Int) { + repo.setInitialOrUpgradeLargeTiles(specs, user) + } + + suspend fun deleteLargeTilesDataJob() { + repo.deleteLargeTileDataJob() } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/startable/QSPanelsCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/startable/QSPanelsCoreStartable.kt index a8ac5c34d8f9..e2797356fa96 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/startable/QSPanelsCoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/startable/QSPanelsCoreStartable.kt @@ -19,11 +19,13 @@ package com.android.systemui.qs.panels.domain.startable import com.android.app.tracing.coroutines.launchTraced import com.android.systemui.CoreStartable import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.qs.flags.QsInCompose import com.android.systemui.qs.panels.domain.interactor.QSPreferencesInteractor import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch class QSPanelsCoreStartable @Inject @@ -33,10 +35,14 @@ constructor( @Background private val backgroundApplicationScope: CoroutineScope, ) : CoreStartable { override fun start() { - backgroundApplicationScope.launchTraced("QSPanelsCoreStartable.startingLargeTiles") { - tileSpecRepository.tilesReadFromSetting.receiveAsFlow().collect { (tiles, userId) -> - preferenceInteractor.setInitialLargeTilesSpecs(tiles, userId) + if (QsInCompose.isEnabled) { + backgroundApplicationScope.launchTraced("QSPanelsCoreStartable.startingLargeTiles") { + tileSpecRepository.tilesUpgradePath.receiveAsFlow().collect { (tiles, userId) -> + preferenceInteractor.setInitialOrUpgradeLargeTilesSpecs(tiles, userId) + } } + } else { + backgroundApplicationScope.launch { preferenceInteractor.deleteLargeTilesDataJob() } } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt index 6b7dd386bb46..c50d5dad10c1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt @@ -24,6 +24,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.qs.pipeline.data.model.RestoreData import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.TilesUpgradePath import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger import com.android.systemui.res.R import com.android.systemui.retail.data.repository.RetailModeRepository @@ -78,7 +79,7 @@ interface TileSpecRepository { /** Reset the current set of tiles to the default list of tiles */ suspend fun resetToDefault(userId: Int) - val tilesReadFromSetting: ReceiveChannel<Pair<Set<TileSpec>, Int>> + val tilesUpgradePath: ReceiveChannel<Pair<TilesUpgradePath, Int>> companion object { /** Position to indicate the end of the list */ @@ -112,8 +113,8 @@ constructor( .filter { it !is TileSpec.Invalid } } - private val _tilesReadFromSetting = Channel<Pair<Set<TileSpec>, Int>>(capacity = 5) - override val tilesReadFromSetting = _tilesReadFromSetting + private val _tilesUpgradePath = Channel<Pair<TilesUpgradePath, Int>>(capacity = 5) + override val tilesUpgradePath = _tilesUpgradePath private val userTileRepositories = SparseArray<UserTileSpecRepository>() @@ -122,8 +123,8 @@ constructor( val userTileRepository = userTileSpecRepositoryFactory.create(userId) userTileRepositories.put(userId, userTileRepository) applicationScope.launchTraced("TileSpecRepository.aggregateTilesPerUser") { - for (tilesFromSettings in userTileRepository.tilesReadFromSettings) { - _tilesReadFromSetting.send(tilesFromSettings to userId) + for (tileUpgrade in userTileRepository.tilesUpgradePath) { + _tilesUpgradePath.send(tileUpgrade to userId) } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt index 7b56cd92a081..5aa5edaa726e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt @@ -9,6 +9,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.qs.pipeline.data.model.RestoreData import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.TilesUpgradePath import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger import com.android.systemui.util.settings.SecureSettings import dagger.assisted.Assisted @@ -49,8 +50,8 @@ constructor( @Background private val backgroundDispatcher: CoroutineDispatcher, ) { - private val _tilesReadFromSettings = Channel<Set<TileSpec>>(capacity = 2) - val tilesReadFromSettings: ReceiveChannel<Set<TileSpec>> = _tilesReadFromSettings + private val _tilesUpgradePath = Channel<TilesUpgradePath>(capacity = 3) + val tilesUpgradePath: ReceiveChannel<TilesUpgradePath> = _tilesUpgradePath private val defaultTiles: List<TileSpec> get() = defaultTilesRepository.defaultTiles @@ -67,14 +68,23 @@ constructor( .scan(loadTilesFromSettingsAndParse(userId)) { current, change -> change .apply(current) - .also { - if (current != it) { + .also { afterRestore -> + if (current != afterRestore) { if (change is RestoreTiles) { - logger.logTilesRestoredAndReconciled(current, it, userId) + logger.logTilesRestoredAndReconciled( + current, + afterRestore, + userId, + ) } else { - logger.logProcessTileChange(change, it, userId) + logger.logProcessTileChange(change, afterRestore, userId) } } + if (change is RestoreTiles) { + _tilesUpgradePath.send( + TilesUpgradePath.RestoreFromBackup(afterRestore.toSet()) + ) + } } // Distinct preserves the order of the elements removing later // duplicates, @@ -154,7 +164,9 @@ constructor( private suspend fun loadTilesFromSettingsAndParse(userId: Int): List<TileSpec> { val loadedTiles = loadTilesFromSettings(userId) if (loadedTiles.isNotEmpty()) { - _tilesReadFromSettings.send(loadedTiles.toSet()) + _tilesUpgradePath.send(TilesUpgradePath.ReadFromSettings(loadedTiles.toSet())) + } else { + _tilesUpgradePath.send(TilesUpgradePath.DefaultSet) } return parseTileSpecs(loadedTiles, userId) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TilesUpgradePath.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TilesUpgradePath.kt new file mode 100644 index 000000000000..98f30c22d0f3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TilesUpgradePath.kt @@ -0,0 +1,37 @@ +/* + * 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.pipeline.shared + +/** Upgrade paths indicating the source of the list of QS tiles. */ +sealed interface TilesUpgradePath { + + sealed interface UpgradeWithTiles : TilesUpgradePath { + val value: Set<TileSpec> + } + + /** This indicates a set of tiles that was read from Settings on user start */ + @JvmInline value class ReadFromSettings(override val value: Set<TileSpec>) : UpgradeWithTiles + + /** This indicates a set of tiles that was restored from backup */ + @JvmInline value class RestoreFromBackup(override val value: Set<TileSpec>) : UpgradeWithTiles + + /** + * This indicates that no tiles were read from Settings on user start so the default has been + * stored. + */ + data object DefaultSet : TilesUpgradePath +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt index 5bc26f50f70f..52c4e2fac6d5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt @@ -25,7 +25,7 @@ import com.android.compose.animation.scene.UserActionResult.ShowOverlay import com.android.compose.animation.scene.UserActionResult.ShowOverlay.HideCurrentOverlays import com.android.systemui.qs.panels.ui.viewmodel.EditModeViewModel import com.android.systemui.scene.shared.model.Overlays -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -47,7 +47,7 @@ constructor(private val editModeViewModel: EditModeViewModel) : UserActionsViewM put(Back, HideOverlay(Overlays.QuickSettingsShade)) } put( - Swipe.Down(fromSource = SceneContainerEdge.TopLeft), + Swipe.Down(fromSource = SceneContainerArea.StartHalf), ShowOverlay( Overlays.NotificationsShade, hideCurrentOverlays = diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt index a4949ad66109..caa61617505f 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt @@ -16,7 +16,6 @@ package com.android.systemui.scene -import androidx.compose.ui.unit.dp import com.android.systemui.CoreStartable import com.android.systemui.notifications.ui.composable.NotificationsShadeSessionModule import com.android.systemui.scene.domain.SceneDomainModule @@ -30,8 +29,6 @@ import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.SceneContainerTransitions -import com.android.systemui.scene.ui.viewmodel.SplitEdgeDetector -import com.android.systemui.shade.domain.interactor.ShadeInteractor import dagger.Binds import dagger.Module import dagger.Provides @@ -99,15 +96,5 @@ interface KeyguardlessSceneContainerFrameworkModule { transitionsBuilder = SceneContainerTransitions(), ) } - - @Provides - fun splitEdgeDetector(shadeInteractor: ShadeInteractor): SplitEdgeDetector { - return SplitEdgeDetector( - topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction, - // TODO(b/338577208): This should be 60dp at the top in the dual-shade UI. Better to - // replace this constant with dynamic window insets. - edgeSize = 40.dp, - ) - } } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt index a018283c3953..ea11d202b119 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt @@ -16,7 +16,6 @@ package com.android.systemui.scene -import androidx.compose.ui.unit.dp import com.android.systemui.CoreStartable import com.android.systemui.notifications.ui.composable.NotificationsShadeSessionModule import com.android.systemui.scene.domain.SceneDomainModule @@ -30,8 +29,6 @@ import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.SceneContainerTransitions -import com.android.systemui.scene.ui.viewmodel.SplitEdgeDetector -import com.android.systemui.shade.domain.interactor.ShadeInteractor import dagger.Binds import dagger.Module import dagger.Provides @@ -121,15 +118,5 @@ interface SceneContainerFrameworkModule { transitionsBuilder = SceneContainerTransitions(), ) } - - @Provides - fun splitEdgeDetector(shadeInteractor: ShadeInteractor): SplitEdgeDetector { - return SplitEdgeDetector( - topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction, - // TODO(b/338577208): This should be 60dp at the top in the dual-shade UI. Better to - // replace this constant with dynamic window insets. - edgeSize = 40.dp, - ) - } } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt index 80c7c4a07c1e..caa7bbae0420 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt @@ -130,6 +130,26 @@ constructor( dataSource.replaceOverlay(from = from, to = to, transitionKey = transitionKey) } + /** + * Instantly shows [overlay]. + * + * The change is instantaneous and not animated; it will be observable in the next frame and + * there will be no transition animation. + */ + fun instantlyShowOverlay(overlay: OverlayKey) { + dataSource.instantlyShowOverlay(overlay) + } + + /** + * Instantly hides [overlay]. + * + * The change is instantaneous and not animated; it will be observable in the next frame and + * there will be no transition animation. + */ + fun instantlyHideOverlay(overlay: OverlayKey) { + dataSource.instantlyHideOverlay(overlay) + } + /** Sets whether the container is visible. */ fun setVisible(isVisible: Boolean) { _isVisible.value = isVisible diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt index 9c04323f2a0e..475c0794861f 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt @@ -33,8 +33,10 @@ import com.android.systemui.log.table.TableRowLogger import com.android.systemui.scene.data.repository.SceneContainerRepository import com.android.systemui.scene.domain.resolver.SceneResolver import com.android.systemui.scene.shared.logger.SceneLogger +import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor import com.android.systemui.util.kotlin.pairwise import dagger.Lazy import javax.inject.Inject @@ -72,6 +74,7 @@ constructor( private val deviceUnlockedInteractor: Lazy<DeviceUnlockedInteractor>, private val keyguardEnabledInteractor: Lazy<KeyguardEnabledInteractor>, private val disabledContentInteractor: DisabledContentInteractor, + private val shadeModeInteractor: ShadeModeInteractor, ) { interface OnSceneAboutToChangeListener { @@ -237,7 +240,13 @@ constructor( ) { val currentSceneKey = currentScene.value val resolvedScene = sceneFamilyResolvers.get()[toScene]?.resolvedScene?.value ?: toScene - if (!validateSceneChange(to = resolvedScene, loggingReason = loggingReason)) { + if ( + !validateSceneChange( + from = currentSceneKey, + to = resolvedScene, + loggingReason = loggingReason, + ) + ) { return } @@ -246,6 +255,7 @@ constructor( logger.logSceneChanged( from = currentSceneKey, to = resolvedScene, + sceneState = sceneState, reason = loggingReason, isInstant = false, ) @@ -269,13 +279,20 @@ constructor( familyResolver.resolvedScene.value } } ?: toScene - if (!validateSceneChange(to = resolvedScene, loggingReason = loggingReason)) { + if ( + !validateSceneChange( + from = currentSceneKey, + to = resolvedScene, + loggingReason = loggingReason, + ) + ) { return } logger.logSceneChanged( from = currentSceneKey, to = resolvedScene, + sceneState = null, reason = loggingReason, isInstant = true, ) @@ -336,6 +353,38 @@ constructor( } /** + * Instantly shows [overlay]. + * + * The change is instantaneous and not animated; it will be observable in the next frame and + * there will be no transition animation. + */ + fun instantlyShowOverlay(overlay: OverlayKey, loggingReason: String) { + if (!validateOverlayChange(to = overlay, loggingReason = loggingReason)) { + return + } + + logger.logOverlayChangeRequested(to = overlay, reason = loggingReason) + + repository.instantlyShowOverlay(overlay) + } + + /** + * Instantly hides [overlay]. + * + * The change is instantaneous and not animated; it will be observable in the next frame and + * there will be no transition animation. + */ + fun instantlyHideOverlay(overlay: OverlayKey, loggingReason: String) { + if (!validateOverlayChange(from = overlay, loggingReason = loggingReason)) { + return + } + + logger.logOverlayChangeRequested(from = overlay, reason = loggingReason) + + repository.instantlyHideOverlay(overlay) + } + + /** * Replace [from] by [to] so that [from] ends up not being visible on screen and [to] ends up * being visible. * @@ -454,11 +503,25 @@ constructor( * Will throw a runtime exception for illegal states (for example, attempting to change to a * scene that's not part of the current scene framework configuration). * + * @param from The current scene being transitioned away from * @param to The desired destination scene to transition to * @param loggingReason The reason why the transition is requested, for logging purposes * @return `true` if the scene change is valid; `false` if it shouldn't happen */ - private fun validateSceneChange(to: SceneKey, loggingReason: String): Boolean { + private fun validateSceneChange(from: SceneKey, to: SceneKey, loggingReason: String): Boolean { + check( + !shadeModeInteractor.isDualShade || (to != Scenes.Shade && to != Scenes.QuickSettings) + ) { + "Can't change scene to ${to.debugName} when dual shade is on!" + } + check(!shadeModeInteractor.isSplitShade || (to != Scenes.QuickSettings)) { + "Can't change scene to ${to.debugName} in split shade mode!" + } + + if (from == to) { + return false + } + if (to !in repository.allContentKeys) { return false } @@ -505,6 +568,13 @@ constructor( " Logging reason for overlay change was: $loggingReason" } + check( + shadeModeInteractor.isDualShade || + (to != Overlays.NotificationsShade && to != Overlays.QuickSettingsShade) + ) { + "Can't show overlay ${to?.debugName} when dual shade is off!" + } + if (to != null && disabledContentInteractor.isDisabled(to)) { return false } diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt index 16c2ef556de8..d00585858ccb 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt @@ -45,23 +45,30 @@ class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer: ) } - fun logSceneChanged(from: SceneKey, to: SceneKey, reason: String, isInstant: Boolean) { + fun logSceneChanged( + from: SceneKey, + to: SceneKey, + sceneState: Any?, + reason: String, + isInstant: Boolean, + ) { logBuffer.log( tag = TAG, level = LogLevel.INFO, messageInitializer = { - str1 = from.toString() - str2 = to.toString() - str3 = reason + str1 = "${from.debugName} → ${to.debugName}" + str2 = reason + str3 = sceneState?.toString() bool1 = isInstant }, messagePrinter = { buildString { - append("Scene changed: $str1 → $str2") + append("Scene changed: $str1") + str3?.let { append(" (sceneState=$it)") } if (isInstant) { append(" (instant)") } - append(", reason: $str3") + append(", reason: $str2") } }, ) diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt index 4538d1ca48f8..daf2d7f698b6 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt @@ -45,17 +45,12 @@ interface SceneDataSource { * Asks for an asynchronous scene switch to [toScene], which will use the corresponding * installed transition or the one specified by [transitionKey], if provided. */ - fun changeScene( - toScene: SceneKey, - transitionKey: TransitionKey? = null, - ) + fun changeScene(toScene: SceneKey, transitionKey: TransitionKey? = null) /** * Asks for an instant scene switch to [toScene], without an animated transition of any kind. */ - fun snapToScene( - toScene: SceneKey, - ) + fun snapToScene(toScene: SceneKey) /** * Request to show [overlay] so that it animates in from [currentScene] and ends up being @@ -64,10 +59,7 @@ interface SceneDataSource { * After this returns, this overlay will be included in [currentOverlays]. This does nothing if * [overlay] is already shown. */ - fun showOverlay( - overlay: OverlayKey, - transitionKey: TransitionKey? = null, - ) + fun showOverlay(overlay: OverlayKey, transitionKey: TransitionKey? = null) /** * Request to hide [overlay] so that it animates out to [currentScene] and ends up *not* being @@ -76,10 +68,7 @@ interface SceneDataSource { * After this returns, this overlay will not be included in [currentOverlays]. This does nothing * if [overlay] is already hidden. */ - fun hideOverlay( - overlay: OverlayKey, - transitionKey: TransitionKey? = null, - ) + fun hideOverlay(overlay: OverlayKey, transitionKey: TransitionKey? = null) /** * Replace [from] by [to] so that [from] ends up not being visible on screen and [to] ends up @@ -87,9 +76,11 @@ interface SceneDataSource { * * This throws if [from] is not currently shown or if [to] is already shown. */ - fun replaceOverlay( - from: OverlayKey, - to: OverlayKey, - transitionKey: TransitionKey? = null, - ) + fun replaceOverlay(from: OverlayKey, to: OverlayKey, transitionKey: TransitionKey? = null) + + /** Asks for [overlay] to be instantly shown, without an animated transition of any kind. */ + fun instantlyShowOverlay(overlay: OverlayKey) + + /** Asks for [overlay] to be instantly hidden, without an animated transition of any kind. */ + fun instantlyHideOverlay(overlay: OverlayKey) } diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt index 5d0edc504782..dcb699539760 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt @@ -31,10 +31,8 @@ import kotlinx.coroutines.flow.stateIn * Delegates calls to a runtime-provided [SceneDataSource] or to a no-op implementation if a * delegate isn't set. */ -class SceneDataSourceDelegator( - applicationScope: CoroutineScope, - config: SceneContainerConfig, -) : SceneDataSource { +class SceneDataSourceDelegator(applicationScope: CoroutineScope, config: SceneContainerConfig) : + SceneDataSource { private val noOpDelegate = NoOpSceneDataSource(config.initialSceneKey) private val delegateMutable = MutableStateFlow<SceneDataSource>(noOpDelegate) @@ -57,38 +55,31 @@ class SceneDataSourceDelegator( ) override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) { - delegateMutable.value.changeScene( - toScene = toScene, - transitionKey = transitionKey, - ) + delegateMutable.value.changeScene(toScene = toScene, transitionKey = transitionKey) } override fun snapToScene(toScene: SceneKey) { - delegateMutable.value.snapToScene( - toScene = toScene, - ) + delegateMutable.value.snapToScene(toScene = toScene) } override fun showOverlay(overlay: OverlayKey, transitionKey: TransitionKey?) { - delegateMutable.value.showOverlay( - overlay = overlay, - transitionKey = transitionKey, - ) + delegateMutable.value.showOverlay(overlay = overlay, transitionKey = transitionKey) } override fun hideOverlay(overlay: OverlayKey, transitionKey: TransitionKey?) { - delegateMutable.value.hideOverlay( - overlay = overlay, - transitionKey = transitionKey, - ) + delegateMutable.value.hideOverlay(overlay = overlay, transitionKey = transitionKey) } override fun replaceOverlay(from: OverlayKey, to: OverlayKey, transitionKey: TransitionKey?) { - delegateMutable.value.replaceOverlay( - from = from, - to = to, - transitionKey = transitionKey, - ) + delegateMutable.value.replaceOverlay(from = from, to = to, transitionKey = transitionKey) + } + + override fun instantlyShowOverlay(overlay: OverlayKey) { + delegateMutable.value.instantlyShowOverlay(overlay) + } + + override fun instantlyHideOverlay(overlay: OverlayKey) { + delegateMutable.value.instantlyHideOverlay(overlay) } /** @@ -105,9 +96,7 @@ class SceneDataSourceDelegator( delegateMutable.value = delegate ?: noOpDelegate } - private class NoOpSceneDataSource( - initialSceneKey: SceneKey, - ) : SceneDataSource { + private class NoOpSceneDataSource(initialSceneKey: SceneKey) : SceneDataSource { override val currentScene: StateFlow<SceneKey> = MutableStateFlow(initialSceneKey).asStateFlow() @@ -125,7 +114,11 @@ class SceneDataSourceDelegator( override fun replaceOverlay( from: OverlayKey, to: OverlayKey, - transitionKey: TransitionKey? + transitionKey: TransitionKey?, ) = Unit + + override fun instantlyShowOverlay(overlay: OverlayKey) = Unit + + override fun instantlyHideOverlay(overlay: OverlayKey) = Unit } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt index f0f476e65e2f..364da5f8e80d 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt @@ -30,19 +30,14 @@ import com.android.systemui.compose.ComposeInitializer import com.android.systemui.res.R /** A view that can serve as the root of the main SysUI window. */ -open class WindowRootView( - context: Context, - attrs: AttributeSet?, -) : - FrameLayout( - context, - attrs, - ) { +open class WindowRootView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) { private lateinit var layoutInsetsController: LayoutInsetsController private var leftInset = 0 private var rightInset = 0 + private var previousInsets: WindowInsets? = null + override fun onAttachedToWindow() { super.onAttachedToWindow() @@ -66,11 +61,14 @@ open class WindowRootView( override fun generateDefaultLayoutParams(): FrameLayout.LayoutParams? { return LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.MATCH_PARENT + FrameLayout.LayoutParams.MATCH_PARENT, ) } override fun onApplyWindowInsets(windowInsets: WindowInsets): WindowInsets? { + if (windowInsets == previousInsets) { + return windowInsets + } val insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars()) if (fitsSystemWindows) { val paddingChanged = insets.top != paddingTop || insets.bottom != paddingBottom @@ -95,7 +93,7 @@ open class WindowRootView( leftInset = pairInsets.first rightInset = pairInsets.second applyMargins() - return windowInsets + return windowInsets.also { previousInsets = WindowInsets(it) } } fun setLayoutInsetsController(layoutInsetsController: LayoutInsetsController) { @@ -143,37 +141,22 @@ open class WindowRootView( interface LayoutInsetsController { /** Update the insets and calculate them accordingly. */ - fun getinsets( - windowInsets: WindowInsets?, - displayCutout: DisplayCutout?, - ): Pair<Int, Int> + fun getinsets(windowInsets: WindowInsets?, displayCutout: DisplayCutout?): Pair<Int, Int> } private class LayoutParams : FrameLayout.LayoutParams { var ignoreRightInset = false - constructor( - width: Int, - height: Int, - ) : super( - width, - height, - ) + constructor(width: Int, height: Int) : super(width, height) @SuppressLint("CustomViewStyleable") - constructor( - context: Context, - attrs: AttributeSet?, - ) : super( - context, - attrs, - ) { + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { val obtainedAttributes = context.obtainStyledAttributes(attrs, R.styleable.StatusBarWindowView_Layout) ignoreRightInset = obtainedAttributes.getBoolean( R.styleable.StatusBarWindowView_Layout_ignoreRightInset, - false + false, ) obtainedAttributes.recycle() } diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetector.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetector.kt new file mode 100644 index 000000000000..ede453dbe6b3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetector.kt @@ -0,0 +1,122 @@ +/* + * 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.scene.ui.viewmodel + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import com.android.compose.animation.scene.Edge +import com.android.compose.animation.scene.FixedSizeEdgeDetector +import com.android.compose.animation.scene.SwipeSource +import com.android.compose.animation.scene.SwipeSourceDetector + +/** Identifies an area of the [SceneContainer] to detect swipe gestures on. */ +sealed class SceneContainerArea(private val resolveArea: (LayoutDirection) -> Resolved) : + SwipeSource { + data object StartEdge : + SceneContainerArea( + resolveArea = { + if (it == LayoutDirection.Ltr) Resolved.LeftEdge else Resolved.RightEdge + } + ) + + data object StartHalf : + SceneContainerArea( + resolveArea = { + if (it == LayoutDirection.Ltr) Resolved.LeftHalf else Resolved.RightHalf + } + ) + + data object EndEdge : + SceneContainerArea( + resolveArea = { + if (it == LayoutDirection.Ltr) Resolved.RightEdge else Resolved.LeftEdge + } + ) + + data object EndHalf : + SceneContainerArea( + resolveArea = { + if (it == LayoutDirection.Ltr) Resolved.RightHalf else Resolved.LeftHalf + } + ) + + override fun resolve(layoutDirection: LayoutDirection): Resolved { + return resolveArea(layoutDirection) + } + + sealed interface Resolved : SwipeSource.Resolved { + data object LeftEdge : Resolved + + data object LeftHalf : Resolved + + data object BottomEdge : Resolved + + data object RightEdge : Resolved + + data object RightHalf : Resolved + } +} + +/** + * A [SwipeSourceDetector] that detects edges similarly to [FixedSizeEdgeDetector], but additionally + * detects the left and right halves of the screen (besides the edges). + * + * Corner cases (literally): A vertical swipe on the top-left corner of the screen will be resolved + * to [SceneContainerArea.Resolved.LeftHalf], whereas a horizontal swipe in the same position will + * be resolved to [SceneContainerArea.Resolved.LeftEdge]. The behavior is similar on the top-right + * corner of the screen. + * + * Callers who need to detect the start and end edges based on the layout direction (LTR vs RTL) + * should subscribe to [SceneContainerArea.StartEdge] and [SceneContainerArea.EndEdge] instead. + * These will be resolved at runtime to [SceneContainerArea.Resolved.LeftEdge] and + * [SceneContainerArea.Resolved.RightEdge] appropriately. Similarly, [SceneContainerArea.StartHalf] + * and [SceneContainerArea.EndHalf] will be resolved appropriately to + * [SceneContainerArea.Resolved.LeftHalf] and [SceneContainerArea.Resolved.RightHalf]. + * + * @param edgeSize The fixed size of each edge. + */ +class SceneContainerSwipeDetector(val edgeSize: Dp) : SwipeSourceDetector { + + private val fixedEdgeDetector = FixedSizeEdgeDetector(edgeSize) + + override fun source( + layoutSize: IntSize, + position: IntOffset, + density: Density, + orientation: Orientation, + ): SceneContainerArea.Resolved { + val fixedEdge = fixedEdgeDetector.source(layoutSize, position, density, orientation) + return when (fixedEdge) { + Edge.Resolved.Left -> SceneContainerArea.Resolved.LeftEdge + Edge.Resolved.Bottom -> SceneContainerArea.Resolved.BottomEdge + Edge.Resolved.Right -> SceneContainerArea.Resolved.RightEdge + else -> { + // Note: This intentionally includes Edge.Resolved.Top. At the moment, we don't need + // to detect swipes on the top edge, and consider them part of the right/left half. + if (position.x < layoutSize.width * 0.5f) { + SceneContainerArea.Resolved.LeftHalf + } else { + SceneContainerArea.Resolved.RightHalf + } + } + } + } +} 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 fbcd8ea9b9e4..01bcc2400933 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 @@ -19,6 +19,7 @@ package com.android.systemui.scene.ui.viewmodel import android.view.MotionEvent import android.view.View import androidx.compose.runtime.getValue +import androidx.compose.ui.unit.dp import com.android.app.tracing.coroutines.launchTraced as launch import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.DefaultEdgeDetector @@ -31,6 +32,8 @@ 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.AodBurnInViewModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.keyguard.ui.viewmodel.LightRevealScrimViewModel import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator @@ -62,12 +65,13 @@ constructor( private val powerInteractor: PowerInteractor, shadeModeInteractor: ShadeModeInteractor, private val remoteInputInteractor: RemoteInputInteractor, - private val splitEdgeDetector: SplitEdgeDetector, private val logger: SceneLogger, hapticsViewModelFactory: SceneContainerHapticsViewModel.Factory, val lightRevealScrim: LightRevealScrimViewModel, val wallpaperViewModel: WallpaperViewModel, keyguardInteractor: KeyguardInteractor, + val burnIn: AodBurnInViewModel, + val clock: KeyguardClockViewModel, @Assisted view: View, @Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit, ) : ExclusiveActivatable() { @@ -85,16 +89,20 @@ constructor( val hapticsViewModel: SceneContainerHapticsViewModel = hapticsViewModelFactory.create(view) /** - * The [SwipeSourceDetector] to use for defining which edges of the screen can be defined in the + * The [SwipeSourceDetector] to use for defining which areas of the screen can be defined in the * [UserAction]s for this container. */ - val edgeDetector: SwipeSourceDetector by + val swipeSourceDetector: SwipeSourceDetector by hydrator.hydratedStateOf( - traceName = "edgeDetector", + traceName = "swipeSourceDetector", initialValue = DefaultEdgeDetector, source = shadeModeInteractor.shadeMode.map { - if (it is ShadeMode.Dual) splitEdgeDetector else DefaultEdgeDetector + if (it is ShadeMode.Dual) { + SceneContainerSwipeDetector(edgeSize = 40.dp) + } else { + DefaultEdgeDetector + } }, ) @@ -237,6 +245,7 @@ constructor( logger.logSceneChanged( from = fromScene, to = toScene, + sceneState = null, reason = "user interaction", isInstant = false, ) diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt deleted file mode 100644 index f88bcb57a27d..000000000000 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.scene.ui.viewmodel - -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.LayoutDirection -import com.android.compose.animation.scene.Edge -import com.android.compose.animation.scene.FixedSizeEdgeDetector -import com.android.compose.animation.scene.SwipeSource -import com.android.compose.animation.scene.SwipeSourceDetector - -/** - * The edge of a [SceneContainer]. It differs from a standard [Edge] by splitting the top edge into - * top-left and top-right. - */ -enum class SceneContainerEdge(private val resolveEdge: (LayoutDirection) -> Resolved) : - SwipeSource { - TopLeft(resolveEdge = { Resolved.TopLeft }), - TopRight(resolveEdge = { Resolved.TopRight }), - TopStart( - resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.TopLeft else Resolved.TopRight } - ), - TopEnd( - resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.TopRight else Resolved.TopLeft } - ), - Bottom(resolveEdge = { Resolved.Bottom }), - Left(resolveEdge = { Resolved.Left }), - Right(resolveEdge = { Resolved.Right }), - Start(resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.Left else Resolved.Right }), - End(resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.Right else Resolved.Left }); - - override fun resolve(layoutDirection: LayoutDirection): Resolved { - return resolveEdge(layoutDirection) - } - - enum class Resolved : SwipeSource.Resolved { - TopLeft, - TopRight, - Bottom, - Left, - Right, - } -} - -/** - * A [SwipeSourceDetector] that detects edges similarly to [FixedSizeEdgeDetector], except that the - * top edge is split in two: top-left and top-right. The split point between the two is dynamic and - * may change during runtime. - * - * Callers who need to detect the start and end edges based on the layout direction (LTR vs RTL) - * should subscribe to [SceneContainerEdge.TopStart] and [SceneContainerEdge.TopEnd] instead. These - * will be resolved at runtime to [SceneContainerEdge.Resolved.TopLeft] and - * [SceneContainerEdge.Resolved.TopRight] appropriately. Similarly, [SceneContainerEdge.Start] and - * [SceneContainerEdge.End] will be resolved appropriately to [SceneContainerEdge.Resolved.Left] and - * [SceneContainerEdge.Resolved.Right]. - * - * @param topEdgeSplitFraction A function which returns the fraction between [0..1] (i.e., - * percentage) of screen width to consider the split point between "top-left" and "top-right" - * edges. It is called on each source detection event. - * @param edgeSize The fixed size of each edge. - */ -class SplitEdgeDetector( - val topEdgeSplitFraction: () -> Float, - val edgeSize: Dp, -) : SwipeSourceDetector { - - private val fixedEdgeDetector = FixedSizeEdgeDetector(edgeSize) - - override fun source( - layoutSize: IntSize, - position: IntOffset, - density: Density, - orientation: Orientation, - ): SceneContainerEdge.Resolved? { - val fixedEdge = - fixedEdgeDetector.source( - layoutSize, - position, - density, - orientation, - ) - return when (fixedEdge) { - Edge.Resolved.Top -> { - val topEdgeSplitFraction = topEdgeSplitFraction() - require(topEdgeSplitFraction in 0f..1f) { - "topEdgeSplitFraction must return a value between 0.0 and 1.0" - } - val isLeftSide = position.x < layoutSize.width * topEdgeSplitFraction - if (isLeftSide) SceneContainerEdge.Resolved.TopLeft - else SceneContainerEdge.Resolved.TopRight - } - Edge.Resolved.Left -> SceneContainerEdge.Resolved.Left - Edge.Resolved.Bottom -> SceneContainerEdge.Resolved.Bottom - Edge.Resolved.Right -> SceneContainerEdge.Resolved.Right - null -> null - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt index a379ef7b0b96..305e71e48702 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt @@ -520,7 +520,10 @@ constructor( val glanceableHubV2 = communalSettingsInteractor.isV2FlagEnabled() if ( !hubShowing && - (touchOnNotifications || touchOnUmo || touchOnSmartspace || glanceableHubV2) + (touchOnNotifications || + touchOnUmo || + touchOnSmartspace || + !communalViewModel.swipeToHubEnabled()) ) { logger.d({ "Lockscreen touch ignored: touchOnNotifications: $bool1, touchOnUmo: $bool2, " + diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index c50c3dc07616..5746cef41d6b 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -110,6 +110,7 @@ import com.android.systemui.keyguard.shared.model.Edge; import com.android.systemui.keyguard.shared.model.TransitionState; import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.keyguard.ui.binder.KeyguardTouchViewBinder; +import com.android.systemui.keyguard.ui.transitions.BlurConfig; import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel; import com.android.systemui.media.controls.domain.pipeline.MediaDataManager; @@ -309,6 +310,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private final AlternateBouncerInteractor mAlternateBouncerInteractor; private final QuickSettingsControllerImpl mQsController; private final TouchHandler mTouchHandler = new TouchHandler(); + private final BlurConfig mBlurConfig; private long mDownTime; private long mStatusBarLongPressDowntime = -1L; @@ -606,7 +608,9 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump PowerInteractor powerInteractor, KeyguardClockPositionAlgorithm keyguardClockPositionAlgorithm, MSDLPlayer msdlPlayer, - BrightnessMirrorShowingInteractor brightnessMirrorShowingInteractor) { + BrightnessMirrorShowingInteractor brightnessMirrorShowingInteractor, + BlurConfig blurConfig) { + mBlurConfig = blurConfig; SceneContainerFlag.assertInLegacyMode(); keyguardStateController.addCallback(new KeyguardStateController.Callback() { @Override @@ -923,8 +927,8 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump if (isBouncerShowing && isExpanded()) { if (mBlurRenderEffect == null) { mBlurRenderEffect = RenderEffect.createBlurEffect( - mDepthController.getMaxBlurRadiusPx(), - mDepthController.getMaxBlurRadiusPx(), + mBlurConfig.getMaxBlurRadiusPx(), + mBlurConfig.getMaxBlurRadiusPx(), Shader.TileMode.CLAMP); } mView.setRenderEffect(mBlurRenderEffect); diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt index b9df9f868dc3..7d4b0ed6304c 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt @@ -45,13 +45,17 @@ import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractorImpl import com.android.systemui.shade.domain.interactor.ShadeModeInteractor import com.android.systemui.shade.domain.interactor.ShadeModeInteractorImpl +import com.android.systemui.window.dagger.WindowRootViewBlurModule import dagger.Binds import dagger.Module import dagger.Provides import javax.inject.Provider /** Module for classes related to the notification shade. */ -@Module(includes = [StartShadeModule::class, ShadeViewProviderModule::class]) +@Module( + includes = + [StartShadeModule::class, ShadeViewProviderModule::class, WindowRootViewBlurModule::class] +) abstract class ShadeModule { companion object { @Provides diff --git a/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt b/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt index b155ada87efd..1f534a5c191a 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt @@ -111,10 +111,7 @@ constructor( statusbarWidth: Int, ): ShadeElement { val xPercentage = motionEvent.x / statusbarWidth - val threshold = shadeInteractor.get().getTopEdgeSplitFraction() - return if (xPercentage < threshold) { - notificationElement.get() - } else qsShadeElement.get() + return if (xPercentage < 0.5f) notificationElement.get() else qsShadeElement.get() } private fun monitorDisplayRemovals(): Job { diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt index 6eaedd73ea76..2b3e4b5db453 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt @@ -34,7 +34,11 @@ constructor( override fun animateCollapseQs(fullyCollapse: Boolean) { if (shadeInteractor.isQsExpanded.value) { val key = - if (fullyCollapse || shadeModeInteractor.isDualShade) { + if ( + fullyCollapse || + shadeModeInteractor.isDualShade || + shadeModeInteractor.isSplitShade + ) { SceneFamilies.Home } else { Scenes.Shade diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt index c8ce316c41dd..6d68796454eb 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt @@ -16,7 +16,6 @@ package com.android.systemui.shade.domain.interactor -import androidx.annotation.FloatRange import com.android.compose.animation.scene.TransitionKey import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -66,16 +65,6 @@ interface ShadeInteractor : BaseShadeInteractor { * wide as the entire screen. */ val isShadeLayoutWide: StateFlow<Boolean> - - /** - * The fraction between [0..1] (i.e., percentage) of screen width to consider the threshold - * between "top-left" and "top-right" for the purposes of dual-shade invocation. - * - * Note that this fraction only determines the *split* between the absolute left and right - * directions. In RTL layouts, the "top-start" edge will resolve to "top-right", and "top-end" - * will resolve to "top-left". - */ - @FloatRange(from = 0.0, to = 1.0) fun getTopEdgeSplitFraction(): Float } /** ShadeInteractor methods with implementations that differ between non-empty impls. */ diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt index b1129a94d833..77e6a833c153 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt @@ -48,8 +48,6 @@ class ShadeInteractorEmptyImpl @Inject constructor() : ShadeInteractor { override val isExpandToQsEnabled: Flow<Boolean> = inactiveFlowBoolean override val isShadeLayoutWide: StateFlow<Boolean> = inactiveFlowBoolean - override fun getTopEdgeSplitFraction(): Float = 0.5f - override fun expandNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) {} override fun expandQuickSettingsShade(loggingReason: String, transitionKey: TransitionKey?) {} diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt index c6752f867183..cf3b08c041be 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt @@ -20,10 +20,11 @@ import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.Swipe import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult +import com.android.compose.animation.scene.UserActionResult.ShowOverlay import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea /** Returns collection of [UserAction] to [UserActionResult] pairs for opening the single shade. */ fun singleShadeActions( @@ -66,11 +67,10 @@ fun splitShadeActions(): Array<Pair<UserAction, UserActionResult>> { /** Returns collection of [UserAction] to [UserActionResult] pairs for opening the dual shade. */ fun dualShadeActions(): Array<Pair<UserAction, UserActionResult>> { - val notifShadeUserActionResult = UserActionResult.ShowOverlay(Overlays.NotificationsShade) - val qsShadeuserActionResult = UserActionResult.ShowOverlay(Overlays.QuickSettingsShade) return arrayOf( - Swipe.Down to notifShadeUserActionResult, - Swipe.Down(fromSource = SceneContainerEdge.TopRight) to qsShadeuserActionResult, + Swipe.Down to ShowOverlay(Overlays.NotificationsShade), + Swipe.Down(fromSource = SceneContainerArea.EndHalf) to + ShowOverlay(Overlays.QuickSettingsShade), ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt b/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt index 04bdfbe00be3..f30043eece62 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar import android.app.ActivityManager +import android.content.res.Resources import android.os.SystemProperties import android.os.Trace import android.os.Trace.TRACE_TAG_APP @@ -28,20 +29,28 @@ import android.view.SurfaceControl import android.view.ViewRootImpl import androidx.annotation.VisibleForTesting import com.android.systemui.Dumpable +import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager import java.io.PrintWriter import javax.inject.Inject import com.android.systemui.keyguard.ui.transitions.BlurConfig +import com.android.systemui.res.R @SysUISingleton open class BlurUtils @Inject constructor( + @Main resources: Resources, blurConfig: BlurConfig, private val crossWindowBlurListeners: CrossWindowBlurListeners, dumpManager: DumpManager ) : Dumpable { val minBlurRadius = blurConfig.minBlurRadiusPx - val maxBlurRadius = blurConfig.maxBlurRadiusPx + val maxBlurRadius = if (Flags.notificationShadeBlur()) { + blurConfig.maxBlurRadiusPx + } else { + resources.getDimensionPixelSize(R.dimen.max_window_blur_radius).toFloat() + } private var lastAppliedBlur = 0 private var earlyWakeupEnabled = false diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java index 25ebc8c1ffa1..f06565f1b6d2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java @@ -24,8 +24,10 @@ import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_UNREDACTED_ import static android.os.Flags.allowPrivateProfile; import static android.os.UserHandle.USER_ALL; import static android.os.UserHandle.USER_NULL; +import static android.provider.Settings.Secure.REDACT_OTP_NOTIFICATION_IMMEDIATELY; import static android.provider.Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS; import static android.provider.Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS; +import static android.provider.Settings.Secure.REDACT_OTP_NOTIFICATION_WHILE_CONNECTED_TO_WIFI; import static com.android.systemui.DejankUtils.whitelistIpcs; @@ -44,6 +46,7 @@ import android.database.ContentObserver; import android.database.ExecutorContentObserver; import android.net.Uri; import android.os.Looper; +import android.os.Process; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; @@ -118,6 +121,11 @@ public class NotificationLockscreenUserManagerImpl implements Settings.Secure.getUriFor(LOCK_SCREEN_SHOW_NOTIFICATIONS); private static final Uri SHOW_PRIVATE_LOCKSCREEN = Settings.Secure.getUriFor(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS); + private static final Uri REDACT_OTP_ON_WIFI = + Settings.Secure.getUriFor(REDACT_OTP_NOTIFICATION_WHILE_CONNECTED_TO_WIFI); + + private static final Uri REDACT_OTP_IMMEDIATELY = + Settings.Secure.getUriFor(REDACT_OTP_NOTIFICATION_IMMEDIATELY); private static final long LOCK_TIME_FOR_SENSITIVE_REDACTION_MS = TimeUnit.MINUTES.toMillis(10); @@ -307,6 +315,9 @@ public class NotificationLockscreenUserManagerImpl implements @VisibleForTesting protected final AtomicBoolean mConnectedToWifi = new AtomicBoolean(false); + protected final AtomicBoolean mRedactOtpOnWifi = new AtomicBoolean(true); + protected final AtomicBoolean mRedactOtpImmediately = new AtomicBoolean(false); + protected int mCurrentUserId = 0; protected NotificationPresenter mPresenter; @@ -363,6 +374,8 @@ public class NotificationLockscreenUserManagerImpl implements mLockScreenUris.add(SHOW_LOCKSCREEN); mLockScreenUris.add(SHOW_PRIVATE_LOCKSCREEN); + mLockScreenUris.add(REDACT_OTP_ON_WIFI); + mLockScreenUris.add(REDACT_OTP_IMMEDIATELY); dumpManager.registerDumpable(this); @@ -432,6 +445,10 @@ public class NotificationLockscreenUserManagerImpl implements changed |= updateUserShowSettings(user.getIdentifier()); } else if (SHOW_PRIVATE_LOCKSCREEN.equals(uri)) { changed |= updateUserShowPrivateSettings(user.getIdentifier()); + } else if (REDACT_OTP_ON_WIFI.equals(uri)) { + changed |= updateRedactOtpOnWifiSetting(); + } else if (REDACT_OTP_IMMEDIATELY.equals(uri)) { + changed |= updateRedactOtpImmediatelySetting(); } } @@ -465,6 +482,14 @@ public class NotificationLockscreenUserManagerImpl implements true, mLockscreenSettingsObserver, USER_ALL); + mSecureSettings.registerContentObserverAsync( + REDACT_OTP_ON_WIFI, + mLockscreenSettingsObserver + ); + mSecureSettings.registerContentObserverAsync( + REDACT_OTP_IMMEDIATELY, + mLockscreenSettingsObserver + ); mBroadcastDispatcher.registerReceiver(mAllUsersReceiver, @@ -602,6 +627,28 @@ public class NotificationLockscreenUserManagerImpl implements } @WorkerThread + private boolean updateRedactOtpOnWifiSetting() { + boolean originalValue = mRedactOtpOnWifi.get(); + boolean newValue = mSecureSettings.getIntForUser( + REDACT_OTP_NOTIFICATION_WHILE_CONNECTED_TO_WIFI, + 0, + Process.myUserHandle().getIdentifier()) != 0; + mRedactOtpOnWifi.set(newValue); + return originalValue != newValue; + } + + @WorkerThread + private boolean updateRedactOtpImmediatelySetting() { + boolean originalValue = mRedactOtpImmediately.get(); + boolean newValue = mSecureSettings.getIntForUser( + REDACT_OTP_NOTIFICATION_IMMEDIATELY, + 0, + Process.myUserHandle().getIdentifier()) != 0; + mRedactOtpImmediately.set(newValue); + return originalValue != newValue; + } + + @WorkerThread private boolean updateGlobalKeyguardSettings() { final boolean oldValue = mKeyguardAllowingNotifications; mKeyguardAllowingNotifications = mKeyguardManager.getPrivateNotificationsAllowed(); @@ -769,23 +816,31 @@ public class NotificationLockscreenUserManagerImpl implements return false; } - if (mConnectedToWifi.get()) { - return false; + if (!mRedactOtpOnWifi.get()) { + if (mConnectedToWifi.get()) { + return false; + } + + long lastWifiConnectTime = mLastWifiConnectionTime.get(); + // If the device has connected to wifi since receiving the notification, do not redact + if (ent.getSbn().getPostTime() < lastWifiConnectTime) { + return false; + } } if (ent.getRanking() == null || !ent.getRanking().hasSensitiveContent()) { return false; } - long lastWifiConnectTime = mLastWifiConnectionTime.get(); - // If the device has connected to wifi since receiving the notification, do not redact - if (ent.getSbn().getPostTime() < lastWifiConnectTime) { - return false; + long latestTimeForRedaction; + if (mRedactOtpImmediately.get()) { + latestTimeForRedaction = mLastLockTime.get(); + } else { + // If the lock screen was not already locked for LOCK_TIME_FOR_SENSITIVE_REDACTION_MS + // when this notification arrived, do not redact + latestTimeForRedaction = mLastLockTime.get() + LOCK_TIME_FOR_SENSITIVE_REDACTION_MS; } - // If the lock screen was not already locked for LOCK_TIME_FOR_SENSITIVE_REDACTION_MS when - // this notification arrived, do not redact - long latestTimeForRedaction = mLastLockTime.get() + LOCK_TIME_FOR_SENSITIVE_REDACTION_MS; if (ent.getSbn().getPostTime() < latestTimeForRedaction) { return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index 155049f512d8..31fdec6147f2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -93,6 +93,7 @@ public class NotificationShelf extends ActivatableNotificationView { private int mPaddingBetweenElements; private int mNotGoneIndex; private boolean mHasItemsInStableShelf; + private boolean mAlignedToEnd; private int mScrollFastThreshold; private boolean mInteractive; private boolean mAnimationsEnabled = true; @@ -412,8 +413,22 @@ public class NotificationShelf extends ActivatableNotificationView { public boolean isAlignedToEnd() { if (!NotificationMinimalism.isEnabled()) { return false; + } else if (SceneContainerFlag.isEnabled()) { + return mAlignedToEnd; + } else { + return mAmbientState.getUseSplitShade(); + } + } + + /** @see #isAlignedToEnd() */ + public void setAlignedToEnd(boolean alignedToEnd) { + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) { + return; + } + if (mAlignedToEnd != alignedToEnd) { + mAlignedToEnd = alignedToEnd; + requestLayout(); } - return mAmbientState.getUseSplitShade(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt index a2c0226addfa..f466278e15a8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt @@ -32,9 +32,7 @@ import com.android.systemui.res.R import com.android.systemui.statusbar.chips.StatusBarChipLogTags.pad import com.android.systemui.statusbar.chips.StatusBarChipsLog import com.android.systemui.statusbar.chips.call.domain.interactor.CallChipInteractor -import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.chips.ui.model.ColorsModel -import com.android.systemui.statusbar.chips.ui.model.ColorsModel.Companion.toCustomColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel @@ -86,12 +84,7 @@ constructor( OngoingActivityChipModel.ChipIcon.SingleColorIcon(phoneIcon) } - val colors = - if (StatusBarNotifChips.isEnabled && state.promotedContent != null) { - state.promotedContent.toCustomColorsModel() - } else { - ColorsModel.Themed - } + val colors = ColorsModel.AccentThemed // This block mimics OngoingCallController#updateChip. if (state.startTimeMs <= 0L) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt index 8357df42937e..2d6102e310f2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt @@ -27,7 +27,7 @@ import com.android.systemui.res.R import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor import com.android.systemui.statusbar.chips.notification.domain.model.NotificationChipModel import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips -import com.android.systemui.statusbar.chips.ui.model.ColorsModel.Companion.toCustomColorsModel +import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor @@ -85,8 +85,7 @@ constructor( contentDescription, ) } - val colors = this.promotedContent.toCustomColorsModel() - + val colors = ColorsModel.SystemThemed val clickListener: () -> Unit = { // The notification pipeline needs everything to run on the main thread, so keep // this event on the main thread. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt index 456cd121a540..d41353b2c176 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.chips.ui.binder import android.annotation.IdRes +import android.content.Context import android.content.res.ColorStateList import android.graphics.drawable.GradientDrawable import android.view.View @@ -32,6 +33,7 @@ import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.res.R import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips +import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer import com.android.systemui.statusbar.chips.ui.view.ChipChronometer @@ -76,8 +78,10 @@ object OngoingActivityChipBinder { chipTimeView.setTextColor(textColor) chipTextView.setTextColor(textColor) chipShortTimeDeltaView.setTextColor(textColor) - (chipBackgroundView.background as GradientDrawable).color = - chipModel.colors.background(chipContext) + (chipBackgroundView.background as GradientDrawable).setBackgroundColors( + chipModel.colors, + chipContext, + ) } is OngoingActivityChipModel.Inactive -> { // The Chronometer should be stopped to prevent leaks -- see b/192243808 and @@ -460,5 +464,20 @@ object OngoingActivityChipBinder { chipView.minimumWidth = minimumWidth } + private fun GradientDrawable.setBackgroundColors(colors: ColorsModel, context: Context) { + this.color = colors.background(context) + val outline = colors.outline(context) + if (outline != null) { + this.setStroke( + context.resources.getDimensionPixelSize( + R.dimen.ongoing_activity_chip_outline_width + ), + outline, + ) + } else { + this.setStroke(0, /* color= */ 0) + } + } + @IdRes private val CUSTOM_ICON_VIEW_ID = R.id.ongoing_activity_chip_custom_icon } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt index 32de0fbfd870..8443d106dfb1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt @@ -20,16 +20,9 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithCache -import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.CompositingStrategy -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.MeasureResult import androidx.compose.ui.layout.MeasureScope @@ -37,6 +30,8 @@ import androidx.compose.ui.node.LayoutModifierNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.text.TextMeasurer +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp @@ -83,15 +78,14 @@ fun ChipContent(viewModel: OngoingActivityChipModel.Active, modifier: Modifier = softWrap = false, modifier = modifier - .customTextContentLayout( + .hideTextIfDoesNotFit( + text = text, + textStyle = textStyle, + textMeasurer = textMeasurer, maxTextWidth = maxTextWidth, startPadding = startPadding, endPadding = endPadding, - ) { constraintWidth -> - val intrinsicWidth = - textMeasurer.measure(text, textStyle, softWrap = false).size.width - intrinsicWidth <= constraintWidth - } + ) .neverDecreaseWidth(), ) } @@ -108,7 +102,6 @@ fun ChipContent(viewModel: OngoingActivityChipModel.Active, modifier: Modifier = } is OngoingActivityChipModel.Active.Text -> { - var hasOverflow by remember { mutableStateOf(false) } val text = viewModel.text Text( text = text, @@ -116,24 +109,14 @@ fun ChipContent(viewModel: OngoingActivityChipModel.Active, modifier: Modifier = style = textStyle, softWrap = false, modifier = - modifier - .customTextContentLayout( - maxTextWidth = maxTextWidth, - startPadding = startPadding, - endPadding = endPadding, - ) { constraintWidth -> - val intrinsicWidth = - textMeasurer.measure(text, textStyle, softWrap = false).size.width - hasOverflow = intrinsicWidth > constraintWidth - constraintWidth.toFloat() / intrinsicWidth.toFloat() > 0.5f - } - .overflowFadeOut( - hasOverflow = { hasOverflow }, - fadeLength = - dimensionResource( - id = R.dimen.ongoing_activity_chip_text_fading_edge_length - ), - ), + modifier.hideTextIfDoesNotFit( + text = text, + textStyle = textStyle, + textMeasurer = textMeasurer, + maxTextWidth = maxTextWidth, + startPadding = startPadding, + endPadding = endPadding, + ), ) } @@ -180,45 +163,67 @@ private class NeverDecreaseWidthNode : Modifier.Node(), LayoutModifierNode { } /** - * A custom layout modifier for text that ensures its text is only visible if a provided - * [shouldShow] callback returns true. Imposes a provided [maxTextWidthPx]. Also, accounts for - * provided padding values if provided and ensures its text is placed with the provided padding - * included around it. + * A custom layout modifier for text that ensures the text is only visible if it completely fits + * within the constrained bounds. Imposes a provided [maxTextWidthPx]. Also, accounts for provided + * padding values if provided and ensures its text is placed with the provided padding included + * around it. */ -private fun Modifier.customTextContentLayout( +private fun Modifier.hideTextIfDoesNotFit( + text: String, + textStyle: TextStyle, + textMeasurer: TextMeasurer, maxTextWidth: Dp, startPadding: Dp = 0.dp, endPadding: Dp = 0.dp, - shouldShow: (constraintWidth: Int) -> Boolean, ): Modifier { return this.then( - CustomTextContentLayoutElement(maxTextWidth, startPadding, endPadding, shouldShow) + HideTextIfDoesNotFitElement( + text, + textStyle, + textMeasurer, + maxTextWidth, + startPadding, + endPadding, + ) ) } -private data class CustomTextContentLayoutElement( +private data class HideTextIfDoesNotFitElement( + val text: String, + val textStyle: TextStyle, + val textMeasurer: TextMeasurer, val maxTextWidth: Dp, val startPadding: Dp, val endPadding: Dp, - val shouldShow: (constrainedWidth: Int) -> Boolean, -) : ModifierNodeElement<CustomTextContentLayoutNode>() { - override fun create(): CustomTextContentLayoutNode { - return CustomTextContentLayoutNode(maxTextWidth, startPadding, endPadding, shouldShow) +) : ModifierNodeElement<HideTextIfDoesNotFitNode>() { + override fun create(): HideTextIfDoesNotFitNode { + return HideTextIfDoesNotFitNode( + text, + textStyle, + textMeasurer, + maxTextWidth, + startPadding, + endPadding, + ) } - override fun update(node: CustomTextContentLayoutNode) { - node.shouldShow = shouldShow + override fun update(node: HideTextIfDoesNotFitNode) { + node.text = text + node.textStyle = textStyle + node.textMeasurer = textMeasurer node.maxTextWidth = maxTextWidth node.startPadding = startPadding node.endPadding = endPadding } } -private class CustomTextContentLayoutNode( +private class HideTextIfDoesNotFitNode( + var text: String, + var textStyle: TextStyle, + var textMeasurer: TextMeasurer, var maxTextWidth: Dp, var startPadding: Dp, var endPadding: Dp, - var shouldShow: (constrainedWidth: Int) -> Boolean, ) : Modifier.Node(), LayoutModifierNode { override fun MeasureScope.measure( measurable: Measurable, @@ -230,9 +235,10 @@ private class CustomTextContentLayoutNode( .coerceAtLeast(constraints.minWidth) val placeable = measurable.measure(constraints.copy(maxWidth = maxWidth)) - val height = placeable.height - val width = placeable.width - return if (shouldShow(maxWidth)) { + val intrinsicWidth = textMeasurer.measure(text, textStyle, softWrap = false).size.width + return if (intrinsicWidth <= maxWidth) { + val height = placeable.height + val width = placeable.width layout(width + horizontalPadding.roundToPx(), height) { placeable.place(startPadding.roundToPx(), 0) } @@ -241,20 +247,3 @@ private class CustomTextContentLayoutNode( } } } - -private fun Modifier.overflowFadeOut(hasOverflow: () -> Boolean, fadeLength: Dp): Modifier { - return graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen).drawWithCache { - val width = size.width - val start = (width - fadeLength.toPx()).coerceAtLeast(0f) - val gradient = - Brush.horizontalGradient( - colors = listOf(Color.Black, Color.Transparent), - startX = start, - endX = width, - ) - onDrawWithContent { - drawContent() - if (hasOverflow()) drawRect(brush = gradient, blendMode = BlendMode.DstIn) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt index 76c53861f0ab..1cdf6800fb97 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.chips.ui.compose import android.content.res.ColorStateList import android.view.ViewGroup import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -103,6 +104,13 @@ private fun ChipBody( } else { dimensionResource(id = R.dimen.ongoing_activity_chip_min_text_width) + chipSidePadding } + + val outline = model.colors.outline(context) + val outlineWidth = dimensionResource(R.dimen.ongoing_activity_chip_outline_width) + + val shape = + RoundedCornerShape(dimensionResource(id = R.dimen.ongoing_activity_chip_corner_radius)) + // Use a Box with `fillMaxHeight` to create a larger click surface for the chip. The visible // height of the chip is determined by the height of the background of the Row below. Box( @@ -121,12 +129,7 @@ private fun ChipBody( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, modifier = - Modifier.clip( - RoundedCornerShape( - dimensionResource(id = R.dimen.ongoing_activity_chip_corner_radius) - ) - ) - .height(dimensionResource(R.dimen.ongoing_appops_chip_height)) + Modifier.height(dimensionResource(R.dimen.ongoing_appops_chip_height)) .thenIf(isClickable) { Modifier.widthIn(min = minWidth) } .layout { measurable, constraints -> val placeable = measurable.measure(constraints) @@ -136,7 +139,14 @@ private fun ChipBody( } } } - .background(Color(model.colors.background(context).defaultColor)) + .background(Color(model.colors.background(context).defaultColor), shape = shape) + .thenIf(outline != null) { + Modifier.border( + width = outlineWidth, + color = Color(outline!!), + shape = shape, + ) + } .padding( horizontal = if (hasEmbeddedIcon) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt index 25f90f9a0065..4954cb0a1b24 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt @@ -21,7 +21,6 @@ import android.content.res.ColorStateList import androidx.annotation.ColorInt import com.android.settingslib.Utils import com.android.systemui.res.R -import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel /** Model representing how the chip in the status bar should be colored. */ sealed interface ColorsModel { @@ -31,13 +30,38 @@ sealed interface ColorsModel { /** The color for the text (and icon) on the chip. */ @ColorInt fun text(context: Context): Int - /** The chip should match the theme's primary color. */ - data object Themed : ColorsModel { + /** The color to use for the chip outline, or null if the chip shouldn't have an outline. */ + @ColorInt fun outline(context: Context): Int? + + /** The chip should match the theme's primary accent color. */ + // TODO(b/347717946): The chip's color isn't getting updated when the user switches theme, it + // only gets updated when a different configuration change happens, like a rotation. + data object AccentThemed : ColorsModel { override fun background(context: Context): ColorStateList = Utils.getColorAttr(context, com.android.internal.R.attr.colorAccent) override fun text(context: Context) = Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorPrimary) + + override fun outline(context: Context) = null + } + + /** The chip should match the system theme main color. */ + // TODO(b/347717946): The chip's color isn't getting updated when the user switches theme, it + // only gets updated when a different configuration change happens, like a rotation. + data object SystemThemed : ColorsModel { + override fun background(context: Context): ColorStateList = + ColorStateList.valueOf( + context.getColor(com.android.internal.R.color.materialColorSurfaceDim) + ) + + override fun text(context: Context) = + context.getColor(com.android.internal.R.color.materialColorOnSurface) + + override fun outline(context: Context) = + // Outline is required on the SystemThemed chip to guarantee the chip doesn't completely + // blend in with the background. + context.getColor(com.android.internal.R.color.materialColorOutlineVariant) } /** The chip should have the given background color and primary text color. */ @@ -46,6 +70,8 @@ sealed interface ColorsModel { ColorStateList.valueOf(backgroundColorInt) override fun text(context: Context): Int = primaryTextColorInt + + override fun outline(context: Context) = null } /** The chip should have a red background with white text. */ @@ -55,15 +81,7 @@ sealed interface ColorsModel { } override fun text(context: Context) = context.getColor(android.R.color.white) - } - companion object { - /** Converts the promoted notification colors to a [Custom] colors model. */ - fun PromotedNotificationContentModel.toCustomColorsModel(): Custom { - return Custom( - backgroundColorInt = this.colors.backgroundColor, - primaryTextColorInt = this.colors.primaryTextColor, - ) - } + override fun outline(context: Context) = null } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipTextTruncationHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipTextTruncationHelper.kt index 52495eb55436..c19b144b7f42 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipTextTruncationHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipTextTruncationHelper.kt @@ -51,9 +51,8 @@ class ChipTextTruncationHelper(private val view: View) { } /** - * Returns true if this view should show the text because there's enough room for a substantial - * amount of text, and returns false if this view should hide the text because the text is much - * too long. + * Returns true if this view should show the text because there's enough room for all the text, + * and returns false if this view should hide the text because not all of it fits. * * @param desiredTextWidthPx should be calculated by having the view measure itself with * [unlimitedWidthMeasureSpec] and then sending its `measuredWidth` to this method. (This @@ -82,9 +81,8 @@ class ChipTextTruncationHelper(private val view: View) { enforcedTextWidth = maxWidthBasedOnDimension } - // Only show the text if at least 50% of it can show. (Assume that if < 50% of the text will - // be visible, the text will be more confusing than helpful.) - return desiredTextWidthPx <= enforcedTextWidth * 2 + // Only show the text if all of it can show + return desiredTextWidthPx <= enforcedTextWidth } private fun fetchMaxWidth() = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt index 3ba0ae3b3cb6..1a30caf0150b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt @@ -214,7 +214,6 @@ constructor( if ( secondaryChip is InternalChipModel.Active && StatusBarNotifChips.isEnabled && - !StatusBarChipsModernization.isEnabled && !isScreenReasonablyLarge ) { // If we have two showing chips and we don't have a ton of room @@ -222,8 +221,10 @@ constructor( // possible so that we have the highest chance of showing both chips (as // opposed to showing the primary chip with a lot of text and completely // hiding the secondary chip). - // Also: If StatusBarChipsModernization is enabled, then we'll do the - // squishing in Compose instead. + // TODO(b/392895330): If StatusBarChipsModernization is enabled, do the + // squishing in Compose instead, and be smart about it (e.g. if we have + // room for the first chip to show text and the second chip to be icon-only, + // do that instead of always squishing both chips.) InternalMultipleOngoingActivityChipsModel( primaryChip.squish(), secondaryChip.squish(), @@ -237,24 +238,31 @@ constructor( /** Squishes the chip down to the smallest content possible. */ private fun InternalChipModel.Active.squish(): InternalChipModel.Active { - return when (model) { + return if (model.shouldSquish()) { + InternalChipModel.Active(this.type, this.model.toIconOnly()) + } else { + this + } + } + + private fun OngoingActivityChipModel.Active.shouldSquish(): Boolean { + return when (this) { // Icon-only is already maximum squished - is OngoingActivityChipModel.Active.IconOnly -> this + is OngoingActivityChipModel.Active.IconOnly, // Countdown shows just a single digit, so already maximum squished - is OngoingActivityChipModel.Active.Countdown -> this - // The other chips have icon+text, so we should hide the text + is OngoingActivityChipModel.Active.Countdown -> false + // The other chips have icon+text, so we can squish them by hiding text is OngoingActivityChipModel.Active.Timer, is OngoingActivityChipModel.Active.ShortTimeDelta, - is OngoingActivityChipModel.Active.Text -> - InternalChipModel.Active(this.type, this.model.toIconOnly()) + is OngoingActivityChipModel.Active.Text -> true } } private fun OngoingActivityChipModel.Active.toIconOnly(): OngoingActivityChipModel.Active { // If this chip doesn't have an icon, then it only has text and we should continue showing // its text. (This is theoretically impossible because - // [OngoingActivityChipModel.Active.Countdown] is the only chip without an icon, but protect - // against it just in case.) + // [OngoingActivityChipModel.Active.Countdown] is the only chip without an icon and + // [shouldSquish] returns false for that model, but protect against it just in case.) val currentIcon = icon ?: return this return OngoingActivityChipModel.Active.IconOnly( key, @@ -271,8 +279,38 @@ constructor( */ val chips: StateFlow<MultipleOngoingActivityChipsModel> = if (StatusBarChipsModernization.isEnabled) { - incomingChipBundle - .map { bundle -> rankChips(bundle) } + combine( + incomingChipBundle.map { bundle -> rankChips(bundle) }, + isScreenReasonablyLarge, + ) { rankedChips, isScreenReasonablyLarge -> + if ( + StatusBarNotifChips.isEnabled && + !isScreenReasonablyLarge && + rankedChips.active.filter { !it.isHidden }.size >= 2 + ) { + // If we have at least two showing chips and we don't have a ton of room + // (!isScreenReasonablyLarge), then we want to make both of them as small as + // possible so that we have the highest chance of showing both chips (as + // opposed to showing the first chip with a lot of text and completely + // hiding the other chips). + val squishedActiveChips = + rankedChips.active.map { + if (!it.isHidden && it.shouldSquish()) { + it.toIconOnly() + } else { + it + } + } + + MultipleOngoingActivityChipsModel( + active = squishedActiveChips, + overflow = rankedChips.overflow, + inactive = rankedChips.inactive, + ) + } else { + rankedChips + } + } .stateIn(scope, SharingStarted.Lazily, MultipleOngoingActivityChipsModel()) } else { MutableStateFlow(MultipleOngoingActivityChipsModel()).asStateFlow() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index 09cc3f23032e..9dc651ed507a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -643,6 +643,10 @@ public final class NotificationEntry extends ListEntry { return row.isMediaRow(); } + public boolean containsCustomViews() { + return getSbn().getNotification().containsCustomViews(); + } + public void resetUserExpansion() { if (row != null) row.resetUserExpansion(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStackModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStackOptionalModule.kt index 6ceeb6aae7a5..bcaf1878a869 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStackModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStackOptionalModule.kt @@ -26,7 +26,7 @@ import dagger.Module * This is meant to be bound in SystemUI variants with [NotificationStackScrollLayoutController]. */ @Module -interface NotificationStackGoogleModule { +interface NotificationStackModule { @Binds fun bindNotificationStackRebindingHider( impl: NotificationStackRebindingHiderImpl @@ -35,7 +35,7 @@ interface NotificationStackGoogleModule { /** This is meant to be used by all SystemUI variants, also those without NSSL. */ @Module -interface NotificationStackModule { +interface NotificationStackOptionalModule { @BindsOptionalOf fun bindOptionalOfNotificationStackRebindingHider(): NotificationStackRebindingHider } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java index e10825bc52fe..34f4969127e3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java @@ -121,7 +121,7 @@ import javax.inject.Provider; NotificationMemoryModule.class, NotificationStatsLoggerModule.class, NotificationsLogModule.class, - NotificationStackModule.class, + NotificationStackOptionalModule.class, }) public interface NotificationsModule { @Binds diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt index de113d365bd8..ccc2dffcfd7b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt @@ -49,7 +49,7 @@ constructor( ) : Dumpable { private val tag = "AvalancheController" - private val debug = Compile.IS_DEBUG + private val debug = Compile.IS_DEBUG && Log.isLoggable(tag, Log.DEBUG) var baseEntryMapStr: () -> String = { "baseEntryMapStr not initialized" } var enableAtRuntime = true diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt index 6491223e6e10..f9e9bee4d809 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt @@ -12,7 +12,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.util.children /** Walks view hiearchy of a given notification to estimate its memory use. */ -internal object NotificationMemoryViewWalker { +object NotificationMemoryViewWalker { private const val TAG = "NotificationMemory" @@ -26,9 +26,13 @@ internal object NotificationMemoryViewWalker { private var softwareBitmaps = 0 fun addSmallIcon(smallIconUse: Int) = apply { smallIcon += smallIconUse } + fun addLargeIcon(largeIconUse: Int) = apply { largeIcon += largeIconUse } + fun addSystem(systemIconUse: Int) = apply { systemIcons += systemIconUse } + fun addStyle(styleUse: Int) = apply { style += styleUse } + fun addSoftwareBitmapPenalty(softwareBitmapUse: Int) = apply { softwareBitmaps += softwareBitmapUse } @@ -67,14 +71,14 @@ internal object NotificationMemoryViewWalker { getViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, row.privateLayout?.expandedChild), getViewUsage( ViewType.PRIVATE_CONTRACTED_VIEW, - row.privateLayout?.contractedChild + row.privateLayout?.contractedChild, ), getViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, row.privateLayout?.headsUpChild), getViewUsage( ViewType.PUBLIC_VIEW, row.publicLayout?.expandedChild, row.publicLayout?.contractedChild, - row.publicLayout?.headsUpChild + row.publicLayout?.headsUpChild, ), ) .filterNotNull() @@ -107,14 +111,14 @@ internal object NotificationMemoryViewWalker { row.publicLayout?.expandedChild, row.publicLayout?.contractedChild, row.publicLayout?.headsUpChild, - seenObjects = seenObjects + seenObjects = seenObjects, ) } private fun getViewUsage( type: ViewType, vararg rootViews: View?, - seenObjects: HashSet<Int> = hashSetOf() + seenObjects: HashSet<Int> = hashSetOf(), ): NotificationViewUsage? { val usageBuilder = lazy { UsageBuilder() } rootViews.forEach { rootView -> 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 7c75983885ea..777ffda8c87d 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 @@ -231,6 +231,7 @@ private class AODPromotedNotificationViewUpdater(root: View) { ) { // Icon binding must be called in this order updateImageView(icon, content.smallIcon) + icon?.setImageLevel(content.iconLevel) icon?.setBackgroundColor(Background.colorInt) icon?.originalIconColor = PrimaryText.colorInt 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 cd7872291801..39c7df064c8c 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 @@ -96,6 +96,7 @@ constructor( contentBuilder.wasPromotedAutomatically = notification.extras.getBoolean(EXTRA_WAS_AUTOMATICALLY_PROMOTED, false) contentBuilder.smallIcon = notification.smallIconModel(imageModelProvider) + contentBuilder.iconLevel = notification.iconLevel contentBuilder.appName = notification.loadHeaderAppName(context) contentBuilder.subText = notification.subText() contentBuilder.time = notification.extractWhen() 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 af5a8203c979..38d41e37f916 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 @@ -38,6 +38,7 @@ data class PromotedNotificationContentModel( */ val wasPromotedAutomatically: Boolean, val smallIcon: ImageModel?, + val iconLevel: Int, val appName: CharSequence?, val subText: CharSequence?, val shortCriticalText: String?, @@ -67,6 +68,7 @@ data class PromotedNotificationContentModel( class Builder(val key: String) { var wasPromotedAutomatically: Boolean = false var smallIcon: ImageModel? = null + var iconLevel: Int = 0 var appName: CharSequence? = null var subText: CharSequence? = null var time: When? = null @@ -94,6 +96,7 @@ data class PromotedNotificationContentModel( identity = Identity(key, style), wasPromotedAutomatically = wasPromotedAutomatically, smallIcon = smallIcon, + iconLevel = iconLevel, appName = appName, subText = subText, shortCriticalText = shortCriticalText, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java index 76ba7f9ea901..2bc48746f847 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java @@ -106,7 +106,7 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable, Ro @Override public void triggerMagneticForce(float endTranslation, @NonNull SpringForce springForce, float startVelocity) { - cancelMagneticAnimations(); + cancelTranslationAnimations(); mMagneticAnimator.setSpring(springForce); mMagneticAnimator.setStartVelocity(startVelocity); mMagneticAnimator.animateToFinalPosition(endTranslation); @@ -114,11 +114,15 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable, Ro @Override public void cancelMagneticAnimations() { - cancelTranslationAnimations(); mMagneticAnimator.cancel(); } @Override + public void cancelTranslationAnimations() { + ExpandableView.this.cancelTranslationAnimations(); + } + + @Override public boolean canRowBeDismissed() { return canExpandableViewBeDismissed(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index c7e15fdb98c7..73e8246907aa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -901,6 +901,13 @@ public class NotificationContentInflater implements NotificationRowContentBinder if (!satisfiesMinHeightRequirement(view, entry, resources)) { return "inflated notification does not meet minimum height requirement"; } + + if (NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry)) { + if (!NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(view, entry)) { + return "inflated notification does not meet maximum memory size requirement"; + } + } + return null; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentCompat.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentCompat.java new file mode 100644 index 000000000000..c55cb6725e45 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentCompat.java @@ -0,0 +1,36 @@ +/* + * 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.statusbar.notification.row; + +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledAfter; +import android.os.Build; + +/** + * Holds compat {@link ChangeId} for {@link NotificationCustomContentMemoryVerifier}. + */ +final class NotificationCustomContentCompat { + /** + * Enables memory size checking of custom views included in notifications to ensure that + * they conform to the size limit set in `config_notificationStripRemoteViewSizeBytes` + * config.xml parameter. + * Notifications exceeding the size will be rejected. + */ + @ChangeId + @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.BAKLAVA) + public static final long CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS = 270553691L; +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifier.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifier.kt new file mode 100644 index 000000000000..a3e6a5cddc94 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifier.kt @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row + +import android.app.compat.CompatChanges +import android.content.Context +import android.graphics.drawable.AdaptiveIconDrawable +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.os.Build +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import androidx.annotation.VisibleForTesting +import com.android.app.tracing.traceSection +import com.android.systemui.statusbar.notification.collection.NotificationEntry + +/** Checks whether Notifications with Custom content views conform to configured memory limits. */ +object NotificationCustomContentMemoryVerifier { + + private const val NOTIFICATION_SERVICE_TAG = "NotificationService" + + /** Notifications with custom views need to conform to maximum memory consumption. */ + @JvmStatic + fun requiresImageViewMemorySizeCheck(entry: NotificationEntry): Boolean { + if (!com.android.server.notification.Flags.notificationCustomViewUriRestriction()) { + return false + } + + return entry.containsCustomViews() + } + + /** + * This walks the custom view hierarchy contained in the passed Notification view and determines + * if the total memory consumption of all image views satisfies the limit set by + * [getStripViewSizeLimit]. It will also log to logcat if the limit exceeds + * [getWarnViewSizeLimit]. + * + * @return true if the Notification conforms to the view size limits. + */ + @JvmStatic + fun satisfiesMemoryLimits(view: View, entry: NotificationEntry): Boolean { + val mainColumnView = + view.findViewById<View>(com.android.internal.R.id.notification_main_column) + if (mainColumnView == null) { + Log.wtf( + NOTIFICATION_SERVICE_TAG, + "R.id.notification_main_column view should not be null!", + ) + return true + } + + val memorySize = + traceSection("computeViewHiearchyImageViewSize") { + computeViewHierarchyImageViewSize(view) + } + + if (memorySize > getStripViewSizeLimit(view.context)) { + val stripOversizedView = isCompatChangeEnabledForUid(entry.sbn.uid) + if (stripOversizedView) { + Log.w( + NOTIFICATION_SERVICE_TAG, + "Dropped notification due to too large RemoteViews ($memorySize bytes) on " + + "pkg: ${entry.sbn.packageName} tag: ${entry.sbn.tag} id: ${entry.sbn.id}", + ) + } else { + Log.w( + NOTIFICATION_SERVICE_TAG, + "RemoteViews too large on pkg: ${entry.sbn.packageName} " + + "tag: ${entry.sbn.tag} id: ${entry.sbn.id} " + + "this WILL notification WILL be dropped when targetSdk " + + "is set to ${Build.VERSION_CODES.BAKLAVA}!", + ) + } + + // We still warn for size, but return "satisfies = ok" if the target SDK + // is too low. + return !stripOversizedView + } + + if (memorySize > getWarnViewSizeLimit(view.context)) { + // We emit the same warning as NotificationManagerService does to keep some consistency + // for developers. + Log.w( + NOTIFICATION_SERVICE_TAG, + "RemoteViews too large on pkg: ${entry.sbn.packageName} " + + "tag: ${entry.sbn.tag} id: ${entry.sbn.id} " + + "this notifications might be dropped in a future release", + ) + } + return true + } + + private fun isCompatChangeEnabledForUid(uid: Int): Boolean = + try { + CompatChanges.isChangeEnabled( + NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS, + uid, + ) + } catch (e: RuntimeException) { + Log.wtf(NOTIFICATION_SERVICE_TAG, "Failed to contact system_server for compat change.") + false + } + + @VisibleForTesting + @JvmStatic + fun computeViewHierarchyImageViewSize(view: View): Int = + when (view) { + is ViewGroup -> { + var use = 0 + for (i in 0 until view.childCount) { + use += computeViewHierarchyImageViewSize(view.getChildAt(i)) + } + use + } + is ImageView -> computeImageViewSize(view) + else -> 0 + } + + /** + * Returns the memory size of a Bitmap contained in a passed [ImageView] in bytes. If the view + * contains any other kind of drawable, the memory size is estimated from its intrinsic + * dimensions. + * + * @return Bitmap size in bytes or 0 if no drawable is set. + */ + private fun computeImageViewSize(view: ImageView): Int { + val drawable = view.drawable + return computeDrawableSize(drawable) + } + + private fun computeDrawableSize(drawable: Drawable?): Int { + return when (drawable) { + null -> 0 + is AdaptiveIconDrawable -> + computeDrawableSize(drawable.foreground) + + computeDrawableSize(drawable.background) + + computeDrawableSize(drawable.monochrome) + is BitmapDrawable -> drawable.bitmap.allocationByteCount + // People can sneak large drawables into those custom memory views via resources - + // we use the intrisic size as a proxy for how much memory rendering those will + // take. + else -> drawable.intrinsicWidth * drawable.intrinsicHeight * 4 + } + } + + /** @return Size of remote views after which a size warning is logged. */ + @VisibleForTesting + fun getWarnViewSizeLimit(context: Context): Int = + context.resources.getInteger( + com.android.internal.R.integer.config_notificationWarnRemoteViewSizeBytes + ) + + /** @return Size of remote views after which the notification is dropped. */ + @VisibleForTesting + fun getStripViewSizeLimit(context: Context): Int = + context.resources.getInteger( + com.android.internal.R.integer.config_notificationStripRemoteViewSizeBytes + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java index bea14b2c003f..49b682d0a5d2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java @@ -83,20 +83,6 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G private static final String TAG = "InfoGuts"; private int mActualHeight; - @IntDef(prefix = { "ACTION_" }, value = { - ACTION_NONE, - ACTION_TOGGLE_ALERT, - ACTION_TOGGLE_SILENT, - }) - public @interface NotificationInfoAction { - } - - public static final int ACTION_NONE = 0; - // standard controls - static final int ACTION_TOGGLE_SILENT = 2; - // standard controls - private static final int ACTION_TOGGLE_ALERT = 5; - private TextView mPriorityDescriptionView; private TextView mSilentDescriptionView; private TextView mAutomaticDescriptionView; @@ -123,7 +109,8 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G * The last importance level chosen by the user. Null if the user has not chosen an importance * level; non-null once the user takes an action which indicates an explicit preference. */ - @Nullable private Integer mChosenImportance; + @Nullable + private Integer mChosenImportance; private boolean mIsAutomaticChosen; private boolean mIsSingleDefaultChannel; private boolean mIsNonblockable; @@ -143,27 +130,27 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G boolean mSkipPost = false; // used by standard ui - private OnClickListener mOnAutomatic = v -> { + private final OnClickListener mOnAutomatic = v -> { mIsAutomaticChosen = true; applyAlertingBehavior(BEHAVIOR_AUTOMATIC, true /* userTriggered */); }; // used by standard ui - private OnClickListener mOnAlert = v -> { + private final OnClickListener mOnAlert = v -> { mChosenImportance = IMPORTANCE_DEFAULT; mIsAutomaticChosen = false; applyAlertingBehavior(BEHAVIOR_ALERTING, true /* userTriggered */); }; // used by standard ui - private OnClickListener mOnSilent = v -> { + private final OnClickListener mOnSilent = v -> { mChosenImportance = IMPORTANCE_LOW; mIsAutomaticChosen = false; applyAlertingBehavior(BEHAVIOR_SILENT, true /* userTriggered */); }; // used by standard ui - private OnClickListener mOnDismissSettings = v -> { + private final OnClickListener mOnDismissSettings = v -> { mPressedApply = true; mGutsContainer.closeControls(v, /* save= */ true); }; @@ -181,13 +168,6 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G mAutomaticDescriptionView = findViewById(R.id.automatic_summary); } - // Specify a CheckSaveListener to override when/if the user's changes are committed. - public interface CheckSaveListener { - // Invoked when importance has changed and the NotificationInfo wants to try to save it. - // Listener should run saveImportance unless the change should be canceled. - void checkSave(Runnable saveImportance, StatusBarNotification sbn); - } - public interface OnSettingsClickListener { void onClick(View v, NotificationChannel channel, int appUid); } @@ -216,7 +196,8 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G boolean isNonblockable, boolean wasShownHighPriority, AssistantFeedbackController assistantFeedbackController, - MetricsLogger metricsLogger, OnClickListener onCloseClick) + MetricsLogger metricsLogger, + OnClickListener onCloseClick) throws RemoteException { mINotificationManager = iNotificationManager; mMetricsLogger = metricsLogger; @@ -623,7 +604,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G intent, PackageManager.MATCH_DEFAULT_ONLY ); - if (resolveInfos == null || resolveInfos.size() == 0 || resolveInfos.get(0) == null) { + if (resolveInfos == null || resolveInfos.isEmpty() || resolveInfos.get(0) == null) { return null; } final ActivityInfo activityInfo = resolveInfos.get(0).activityInfo; @@ -758,6 +739,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G /** * Returns a LogMaker with all available notification information. * Caller should set category, type, and maybe subtype, before passing it to mMetricsLogger. + * * @return LogMaker */ private LogMaker getLogMaker() { @@ -769,10 +751,11 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G /** * Returns an initialized LogMaker for logging importance changes. * The caller may override the type before passing it to mMetricsLogger. + * * @return LogMaker */ private LogMaker importanceChangeLogMaker() { - Integer chosenImportance = + int chosenImportance = mChosenImportance != null ? mChosenImportance : mStartingChannelImportance; return getLogMaker().setCategory(MetricsEvent.ACTION_SAVE_IMPORTANCE) .setType(MetricsEvent.TYPE_ACTION) @@ -782,6 +765,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G /** * Returns an initialized LogMaker for logging open/close of the info display. * The caller may override the type before passing it to mMetricsLogger. + * * @return LogMaker */ private LogMaker notificationControlsLogMaker() { @@ -799,7 +783,9 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G @Retention(SOURCE) @IntDef({BEHAVIOR_ALERTING, BEHAVIOR_SILENT, BEHAVIOR_AUTOMATIC}) - private @interface AlertingBehavior {} + private @interface AlertingBehavior { + } + private static final int BEHAVIOR_ALERTING = 0; private static final int BEHAVIOR_SILENT = 1; private static final int BEHAVIOR_AUTOMATIC = 2; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt index 20c3464536e9..589e5b8be240 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt @@ -1396,9 +1396,17 @@ constructor( */ @VisibleForTesting fun isValidView(view: View, entry: NotificationEntry, resources: Resources): String? { - return if (!satisfiesMinHeightRequirement(view, entry, resources)) { - "inflated notification does not meet minimum height requirement" - } else null + if (!satisfiesMinHeightRequirement(view, entry, resources)) { + return "inflated notification does not meet minimum height requirement" + } + + if (NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry)) { + if (!NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(view, entry)) { + return "inflated notification does not meet maximum memory size requirement" + } + } + + return null } private fun satisfiesMinHeightRequirement( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt index 9fdd0bcc4ee9..0703f2de250d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt @@ -21,11 +21,14 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor +import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.LockscreenShadeTransitionController import com.android.systemui.statusbar.NotificationShelf import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map /** Interactor for the [NotificationShelf] */ @SysUISingleton @@ -35,6 +38,7 @@ constructor( private val keyguardRepository: KeyguardRepository, private val deviceEntryFaceAuthRepository: DeviceEntryFaceAuthRepository, private val powerInteractor: PowerInteractor, + private val shadeModeInteractor: ShadeModeInteractor, private val keyguardTransitionController: LockscreenShadeTransitionController, ) { /** Is the shelf showing on the keyguard? */ @@ -51,6 +55,16 @@ constructor( isKeyguardShowing && isBypassEnabled } + /** Should the shelf be aligned to the end in the current configuration? */ + val isAlignedToEnd: Flow<Boolean> + get() = + shadeModeInteractor.shadeMode.map { shadeMode -> + when (shadeMode) { + ShadeMode.Split -> true + else -> false + } + } + /** Transition keyguard to the locked shade, triggered by the shelf. */ fun goToLockedShadeFromShelf() { powerInteractor.wakeUpIfDozing("SHADE_CLICK", PowerManager.WAKE_REASON_GESTURE) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt index 0352a304a5c1..f663ea019319 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt @@ -16,15 +16,16 @@ package com.android.systemui.statusbar.notification.shelf.ui.viewbinder +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.app.tracing.traceSection import com.android.systemui.plugins.FalsingManager +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.statusbar.NotificationShelf import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerShelfViewBinder import com.android.systemui.statusbar.notification.row.ui.viewbinder.ActivatableNotificationViewBinder import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope -import com.android.app.tracing.coroutines.launchTraced as launch /** Binds a [NotificationShelf] to its [view model][NotificationShelfViewModel]. */ object NotificationShelfViewBinder { @@ -41,6 +42,11 @@ object NotificationShelfViewBinder { viewModel.canModifyColorOfNotifications.collect(::setCanModifyColorOfNotifications) } launch { viewModel.isClickable.collect(::setCanInteract) } + + if (SceneContainerFlag.isEnabled) { + launch { viewModel.isAlignedToEnd.collect(::setAlignedToEnd) } + } + registerViewListenersWhileAttached(shelf, viewModel) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt index 5ca8b53d0704..96cdda6d4a23 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt @@ -17,11 +17,13 @@ package com.android.systemui.statusbar.notification.shelf.ui.viewmodel import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.statusbar.NotificationShelf import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModel import com.android.systemui.statusbar.notification.shelf.domain.interactor.NotificationShelfInteractor import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map /** ViewModel for [NotificationShelf]. */ @@ -40,6 +42,15 @@ constructor( val canModifyColorOfNotifications: Flow<Boolean> get() = interactor.isShelfStatic.map { static -> !static } + /** Is the shelf aligned to the end in the current configuration? */ + val isAlignedToEnd: Flow<Boolean> by lazy { + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) { + flowOf(false) + } else { + interactor.isAlignedToEnd + } + } + /** Notifies that the user has clicked the shelf. */ fun onShelfClicked() { interactor.goToLockedShadeFromShelf() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt index 3941700496f4..5a29a699a7e6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt @@ -97,6 +97,7 @@ constructor( stackScrollLayout, MAGNETIC_TRANSLATION_MULTIPLIERS.size, ) + currentMagneticListeners.swipedListener()?.cancelTranslationAnimations() newListeners.forEach { if (currentMagneticListeners.contains(it)) { it?.cancelMagneticAnimations() @@ -214,22 +215,32 @@ constructor( } override fun onMagneticInteractionEnd(row: ExpandableNotificationRow, velocity: Float?) { - if (!row.isSwipedTarget()) return - - when (currentState) { - State.PULLING -> { - snapNeighborsBack(velocity) - currentState = State.IDLE - } - State.DETACHED -> { - currentState = State.IDLE + if (row.isSwipedTarget()) { + when (currentState) { + State.PULLING -> { + snapNeighborsBack(velocity) + currentState = State.IDLE + } + State.DETACHED -> { + // Cancel any detaching animation that may be occurring + currentMagneticListeners.swipedListener()?.cancelMagneticAnimations() + currentState = State.IDLE + } + else -> {} } - else -> {} + } else { + // A magnetic neighbor may be dismissing. In this case, we need to cancel any snap back + // magnetic animation to let the external dismiss animation proceed. + val listener = currentMagneticListeners.find { it == row.magneticRowListener } + listener?.cancelMagneticAnimations() } } override fun reset() { - currentMagneticListeners.forEach { it?.cancelMagneticAnimations() } + currentMagneticListeners.forEach { + it?.cancelMagneticAnimations() + it?.cancelTranslationAnimations() + } currentState = State.IDLE currentMagneticListeners = listOf() currentRoundableTargets = null diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt index 46036d4c1fad..5959ef1e093b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt @@ -42,6 +42,9 @@ interface MagneticRowListener { /** Cancel any animations related to the magnetic interactions of the row */ fun cancelMagneticAnimations() + /** Cancel any other animations related to the row's translation */ + fun cancelTranslationAnimations() + /** Can the row be dismissed. */ fun canRowBeDismissed(): Boolean } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 48a6a4c057df..810d0b43b0dd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -463,6 +463,13 @@ public class NotificationStackScrollLayoutController implements Dumpable { } @Override + public void onMagneticInteractionEnd(View view, float velocity) { + if (view instanceof ExpandableNotificationRow row) { + mMagneticNotificationRowManager.onMagneticInteractionEnd(row, velocity); + } + } + + @Override public float getTotalTranslationLength(View animView) { return mView.getTotalTranslationLength(animView); } @@ -504,14 +511,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { public void onDragCancelled(View v) { } - @Override - public void onDragCancelledWithVelocity(View v, float finalVelocity) { - if (v instanceof ExpandableNotificationRow row) { - mMagneticNotificationRowManager.onMagneticInteractionEnd( - row, finalVelocity); - } - } - /** * Handles cleanup after the given {@code view} has been fully swiped out (including * re-invoking dismiss logic in case the notification has not made its way out yet). @@ -539,10 +538,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { */ public void handleChildViewDismissed(View view) { - if (view instanceof ExpandableNotificationRow row) { - mMagneticNotificationRowManager.onMagneticInteractionEnd( - row, null /* velocity */); - } // The View needs to clean up the Swipe states, e.g. roundness. mView.onSwipeEnd(); if (mView.getClearAllInProgress()) { @@ -614,11 +609,15 @@ public class NotificationStackScrollLayoutController implements Dumpable { @Override public void onBeginDrag(View v) { + mView.onSwipeBegin(v); + } + + @Override + public void setMagneticAndRoundableTargets(View v) { if (v instanceof ExpandableNotificationRow row) { mMagneticNotificationRowManager.setMagneticAndRoundableTargets( row, mView, mSectionsManager); } - mView.onSwipeBegin(v); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java index d476d482226d..6f4047f48205 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java @@ -362,7 +362,8 @@ class NotificationSwipeHelper extends SwipeHelper implements NotificationSwipeAc superSnapChild(animView, targetLeft, velocity); } - mCallback.onDragCancelledWithVelocity(animView, velocity); + mCallback.onMagneticInteractionEnd(animView, velocity); + mCallback.onDragCancelled(animView); if (targetLeft == 0) { handleMenuCoveredOrDismissed(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index 1bcc5adea6e8..54efa4a2bcf2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -478,7 +478,7 @@ constructor( /** * Ensure view is visible when the shade/qs are expanded. Also, as QS is expanding, fade out - * notifications unless in splitshade. + * notifications unless it's a large screen. */ private val alphaForShadeAndQsExpansion: Flow<Float> = if (SceneContainerFlag.isEnabled) { @@ -501,16 +501,26 @@ constructor( Split -> isAnyExpanded.filter { it }.map { 1f } Dual -> combineTransform( + shadeModeInteractor.isShadeLayoutWide, headsUpNotificationInteractor.get().isHeadsUpOrAnimatingAway, shadeInteractor.shadeExpansion, shadeInteractor.qsExpansion, - ) { isHeadsUpOrAnimatingAway, shadeExpansion, qsExpansion -> - if (isHeadsUpOrAnimatingAway) { + ) { + isShadeLayoutWide, + isHeadsUpOrAnimatingAway, + shadeExpansion, + qsExpansion -> + if (isShadeLayoutWide) { + if (shadeExpansion > 0f) { + emit(1f) + } + } else if (isHeadsUpOrAnimatingAway) { // Ensure HUNs will be visible in QS shade (at least while // unlocked) emit(1f) } else if (shadeExpansion > 0f || qsExpansion > 0f) { - // Fade out as QS shade expands + // On a narrow screen, the QS shade overlaps with lockscreen + // notifications. Fade them out as the QS shade expands. emit(1f - qsExpansion) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java index c9a40f8ab5a5..fa4fe46e690c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java @@ -62,6 +62,7 @@ import kotlinx.coroutines.flow.StateFlowKt; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Objects; /** * The header group on Keyguard. @@ -104,6 +105,9 @@ public class KeyguardStatusBarView extends RelativeLayout { */ private int mCutoutSideNudge = 0; + @Nullable + private WindowInsets mPreviousInsets = null; + private DisplayCutout mDisplayCutout; private int mRoundedCornerPadding = 0; // right and left padding applied to this view to account for cutouts and rounded corners @@ -293,9 +297,12 @@ public class KeyguardStatusBarView extends RelativeLayout { WindowInsets updateWindowInsets( WindowInsets insets, StatusBarContentInsetsProvider insetsProvider) { - mLayoutState = LAYOUT_NONE; - if (updateLayoutConsideringCutout(insetsProvider)) { - requestLayout(); + if (!Objects.equals(mPreviousInsets, insets)) { + mLayoutState = LAYOUT_NONE; + if (updateLayoutConsideringCutout(insetsProvider)) { + requestLayout(); + } + mPreviousInsets = new WindowInsets(insets); } return super.onApplyWindowInsets(insets); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index 4d222fdb90ea..b2d337797b53 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -81,6 +81,9 @@ import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.util.wakelock.DelayedWakeLock; import com.android.systemui.util.wakelock.WakeLock; +import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor; + +import dagger.Lazy; import kotlinx.coroutines.CoroutineDispatcher; @@ -226,7 +229,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump private float mScrimBehindAlphaKeyguard = KEYGUARD_SCRIM_ALPHA; static final float TRANSPARENT_BOUNCER_SCRIM_ALPHA = 0.54f; - private final float mDefaultScrimAlpha; + private float mDefaultScrimAlpha; private float mRawPanelExpansionFraction; private float mPanelScrimMinFraction; @@ -257,6 +260,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump private final TriConsumer<ScrimState, Float, GradientColors> mScrimStateListener; private final LargeScreenShadeInterpolator mLargeScreenShadeInterpolator; private final BlurConfig mBlurConfig; + private final Lazy<WindowRootViewBlurInteractor> mWindowRootViewBlurInteractor; private Consumer<Integer> mScrimVisibleListener; private boolean mBlankScreen; private boolean mScreenBlankingCallbackCalled; @@ -339,14 +343,13 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump KeyguardInteractor keyguardInteractor, @Main CoroutineDispatcher mainDispatcher, LargeScreenShadeInterpolator largeScreenShadeInterpolator, - BlurConfig blurConfig) { + BlurConfig blurConfig, + Lazy<WindowRootViewBlurInteractor> windowRootViewBlurInteractor) { mScrimStateListener = lightBarController::setScrimState; mLargeScreenShadeInterpolator = largeScreenShadeInterpolator; mBlurConfig = blurConfig; - // All scrims default alpha need to match bouncer background alpha to make sure the - // transitions involving the bouncer are smooth and don't overshoot the bouncer alpha. - mDefaultScrimAlpha = - Flags.bouncerUiRevamp() ? TRANSPARENT_BOUNCER_SCRIM_ALPHA : BUSY_SCRIM_ALPHA; + mWindowRootViewBlurInteractor = windowRootViewBlurInteractor; + mDefaultScrimAlpha = BUSY_SCRIM_ALPHA; mKeyguardStateController = keyguardStateController; mDarkenWhileDragging = !mKeyguardStateController.canDismissLockScreen(); @@ -407,7 +410,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump final ScrimState[] states = ScrimState.values(); for (int i = 0; i < states.length; i++) { - states[i].init(mScrimInFront, mScrimBehind, mDozeParameters, mDockManager, mBlurConfig); + states[i].init(mScrimInFront, mScrimBehind, mDozeParameters, mDockManager); states[i].setScrimBehindAlphaKeyguard(mScrimBehindAlphaKeyguard); states[i].setDefaultScrimAlpha(mDefaultScrimAlpha); } @@ -485,6 +488,30 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump Edge.Companion.create(Scenes.Communal, LOCKSCREEN), Edge.Companion.create(GLANCEABLE_HUB, LOCKSCREEN)), mGlanceableHubConsumer, mMainDispatcher); + + if (Flags.bouncerUiRevamp()) { + collectFlow(behindScrim, + mWindowRootViewBlurInteractor.get().isBlurCurrentlySupported(), + this::handleBlurSupportedChanged); + } + } + + private void updateDefaultScrimAlpha(float alpha) { + mDefaultScrimAlpha = alpha; + for (ScrimState state : ScrimState.values()) { + state.setDefaultScrimAlpha(mDefaultScrimAlpha); + } + applyAndDispatchState(); + } + + private void handleBlurSupportedChanged(boolean isBlurSupported) { + if (isBlurSupported) { + updateDefaultScrimAlpha(TRANSPARENT_BOUNCER_SCRIM_ALPHA); + ScrimState.BOUNCER_SCRIMMED.setNotifBlurRadius(mBlurConfig.getMaxBlurRadiusPx()); + } else { + ScrimState.BOUNCER_SCRIMMED.setNotifBlurRadius(0f); + updateDefaultScrimAlpha(BUSY_SCRIM_ALPHA); + } } // TODO(b/270984686) recompute scrim height accurately, based on shade contents. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java index 5f423cf35edd..071a57a8b298 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java @@ -17,14 +17,12 @@ package com.android.systemui.statusbar.phone; import static com.android.systemui.statusbar.phone.ScrimController.BUSY_SCRIM_ALPHA; -import static com.android.systemui.statusbar.phone.ScrimController.TRANSPARENT_BOUNCER_SCRIM_ALPHA; import android.graphics.Color; import com.android.app.tracing.coroutines.TrackTracer; import com.android.systemui.Flags; import com.android.systemui.dock.DockManager; -import com.android.systemui.keyguard.ui.transitions.BlurConfig; import com.android.systemui.res.R; import com.android.systemui.scrim.ScrimView; import com.android.systemui.shade.ui.ShadeColors; @@ -116,8 +114,8 @@ public enum ScrimState { @Override public void prepare(ScrimState previousState) { if (Flags.bouncerUiRevamp()) { - mBehindAlpha = mClipQsScrim ? 0.0f : TRANSPARENT_BOUNCER_SCRIM_ALPHA; - mNotifAlpha = mClipQsScrim ? TRANSPARENT_BOUNCER_SCRIM_ALPHA : 0; + mBehindAlpha = mDefaultScrimAlpha; + mNotifAlpha = 0f; mBehindTint = mNotifTint = mSurfaceColor; mFrontAlpha = 0f; return; @@ -153,12 +151,11 @@ public enum ScrimState { if (previousState == SHADE_LOCKED) { mBehindAlpha = previousState.getBehindAlpha(); mNotifAlpha = previousState.getNotifAlpha(); - mNotifBlurRadius = mBlurConfig.getMaxBlurRadiusPx(); } else { mNotifAlpha = 0f; mBehindAlpha = 0f; } - mFrontAlpha = TRANSPARENT_BOUNCER_SCRIM_ALPHA; + mFrontAlpha = mDefaultScrimAlpha; mFrontTint = mSurfaceColor; return; } @@ -403,7 +400,6 @@ public enum ScrimState { DozeParameters mDozeParameters; DockManager mDockManager; boolean mDisplayRequiresBlanking; - protected BlurConfig mBlurConfig; boolean mLaunchingAffordanceWithPreview; boolean mOccludeAnimationPlaying; boolean mWakeLockScreenSensorActive; @@ -417,7 +413,7 @@ public enum ScrimState { protected float mNotifBlurRadius = 0.0f; public void init(ScrimView scrimInFront, ScrimView scrimBehind, DozeParameters dozeParameters, - DockManager dockManager, BlurConfig blurConfig) { + DockManager dockManager) { mBackgroundColor = scrimBehind.getContext().getColor(R.color.shade_scrim_background_dark); mScrimInFront = scrimInFront; mScrimBehind = scrimBehind; @@ -425,7 +421,6 @@ public enum ScrimState { mDozeParameters = dozeParameters; mDockManager = dockManager; mDisplayRequiresBlanking = dozeParameters.getDisplayNeedsBlanking(); - mBlurConfig = blurConfig; } /** Prepare state for transition. */ @@ -536,4 +531,8 @@ public enum ScrimState { public float getNotifBlurRadius() { return mNotifBlurRadius; } + + public void setNotifBlurRadius(float value) { + mNotifBlurRadius = value; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index b2c4ef95242b..01de925f3d78 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -65,6 +65,7 @@ import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags; import com.android.systemui.bouncer.ui.BouncerView; import com.android.systemui.bouncer.util.BouncerTestUtilsKt; +import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor; @@ -170,6 +171,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private final Lazy<SceneInteractor> mSceneInteractorLazy; private final Lazy<DeviceEntryInteractor> mDeviceEntryInteractorLazy; private final DismissCallbackRegistry mDismissCallbackRegistry; + private final CommunalSceneInteractor mCommunalSceneInteractor; private Job mListenForAlternateBouncerTransitionSteps = null; private Job mListenForKeyguardAuthenticatedBiometricsHandled = null; @@ -406,7 +408,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb @Main DelayableExecutor executor, Lazy<DeviceEntryInteractor> deviceEntryInteractorLazy, DismissCallbackRegistry dismissCallbackRegistry, - Lazy<BouncerInteractor> bouncerInteractor + Lazy<BouncerInteractor> bouncerInteractor, + CommunalSceneInteractor communalSceneInteractor ) { mContext = context; mExecutor = executor; @@ -443,6 +446,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mStatusBarKeyguardViewManagerInteractor = statusBarKeyguardViewManagerInteractor; mDeviceEntryInteractorLazy = deviceEntryInteractorLazy; mDismissCallbackRegistry = dismissCallbackRegistry; + mCommunalSceneInteractor = communalSceneInteractor; } KeyguardTransitionInteractor mKeyguardTransitionInteractor; @@ -1364,11 +1368,13 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } mStatusBarStateController.setLeaveOpenOnKeyguardHide(false); - boolean hideBouncerOverDream = isBouncerShowing() - && mDreamOverlayStateController.isOverlayActive(); + boolean hideBouncerOverDreamOrHub = isBouncerShowing() + && (mDreamOverlayStateController.isOverlayActive() + || mCommunalSceneInteractor.isIdleOnCommunal().getValue()); mCentralSurfaces.endAffordanceLaunch(); // The second condition is for SIM card locked bouncer - if (hideBouncerOverDream || (primaryBouncerIsScrimmed() && !needsFullscreenBouncer())) { + if (hideBouncerOverDreamOrHub + || (primaryBouncerIsScrimmed() && !needsFullscreenBouncer())) { hideBouncer(false); updateStates(); } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index e33baf7c33ae..ded964d8a1cc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -57,11 +57,11 @@ import com.android.systemui.animation.ActivityTransitionAnimator; import com.android.systemui.assist.AssistManager; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.DisplayId; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.ShadeController; +import com.android.systemui.shade.ShadeDisplayAware; import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor; import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor; import com.android.systemui.statusbar.CommandQueue; @@ -76,11 +76,11 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.provider.LaunchFullScreenIntentProvider; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix; +import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; +import com.android.systemui.statusbar.notification.headsup.HeadsUpUtil; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowDragController; import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback; -import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; -import com.android.systemui.statusbar.notification.headsup.HeadsUpUtil; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.wmshell.BubblesManager; @@ -115,7 +115,6 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit private final static String TAG = "StatusBarNotificationActivityStarter"; private final Context mContext; - private final int mDisplayId; private final Handler mMainThreadHandler; private final Executor mUiBgExecutor; @@ -155,8 +154,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit @Inject StatusBarNotificationActivityStarter( - Context context, - @DisplayId int displayId, + @ShadeDisplayAware Context context, Handler mainThreadHandler, @Background Executor uiBgExecutor, NotificationVisibilityProvider visibilityProvider, @@ -189,7 +187,6 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit PowerInteractor powerInteractor, UserTracker userTracker) { mContext = context; - mDisplayId = displayId; mMainThreadHandler = mainThreadHandler; mUiBgExecutor = uiBgExecutor; mVisibilityProvider = visibilityProvider; @@ -493,6 +490,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit boolean animate, boolean isActivityIntent) { mLogger.logStartNotificationIntent(entry); + final int displayId = mContext.getDisplayId(); try { ActivityTransitionAnimator.Controller animationController = new StatusBarTransitionAnimatorController( @@ -501,7 +499,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mShadeController, mNotificationShadeWindowController, mCommandQueue, - mDisplayId, + displayId, isActivityIntent); mActivityTransitionAnimator.startPendingIntentWithAnimation( animationController, @@ -511,11 +509,11 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit long eventTime = row.getAndResetLastActionUpTime(); Bundle options = eventTime > 0 ? getActivityOptions( - mDisplayId, + displayId, adapter, mKeyguardStateController.isShowing(), eventTime) - : getActivityOptions(mDisplayId, adapter); + : getActivityOptions(displayId, adapter); int result = intent.sendAndReturnResult(mContext, 0, fillInIntent, null, null, null, options); mLogger.logSendPendingIntent(entry, intent, result); @@ -533,6 +531,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit public void startNotificationGutsIntent(@NonNull final Intent intent, final int appUid, @NonNull ExpandableNotificationRow row) { boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */); + final int displayId = mContext.getDisplayId(); ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() { @Override public boolean onDismiss() { @@ -544,7 +543,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mShadeController, mNotificationShadeWindowController, mCommandQueue, - mDisplayId, + displayId, true /* isActivityIntent */); mActivityTransitionAnimator.startIntentWithAnimation( @@ -552,7 +551,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit (adapter) -> TaskStackBuilder.create(mContext) .addNextIntentWithParentStack(intent) .startActivities(getActivityOptions( - mDisplayId, + displayId, adapter), new UserHandle(UserHandle.getUserId(appUid)))); }); @@ -571,6 +570,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit @Override public void startHistoryIntent(View view, boolean showHistory) { ModesEmptyShadeFix.assertInLegacyMode(); + final int displayId = mContext.getDisplayId(); boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */); ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() { @Override @@ -597,13 +597,13 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mShadeController, mNotificationShadeWindowController, mCommandQueue, - mDisplayId, + displayId, true /* isActivityIntent */); mActivityTransitionAnimator.startIntentWithAnimation( animationController, animate, intent.getPackage(), (adapter) -> tsb.startActivities( - getActivityOptions(mDisplayId, adapter), + getActivityOptions(displayId, adapter), mUserTracker.getUserHandle())); }); return true; @@ -620,6 +620,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit @Override public void startSettingsIntent(@NonNull View view, @NonNull SettingsIntent intentInfo) { + final int displayId = mContext.getDisplayId(); boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */); ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() { @Override @@ -642,13 +643,13 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mShadeController, mNotificationShadeWindowController, mCommandQueue, - mDisplayId, + displayId, true /* isActivityIntent */); mActivityTransitionAnimator.startIntentWithAnimation( animationController, animate, intentInfo.getTargetIntent().getPackage(), (adapter) -> tsb.startActivities( - getActivityOptions(mDisplayId, adapter), + getActivityOptions(displayId, adapter), mUserTracker.getUserHandle())); }); return true; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitShadeStateController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitShadeStateController.kt index 72d093c65a91..9f05850f3405 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitShadeStateController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitShadeStateController.kt @@ -22,11 +22,11 @@ interface SplitShadeStateController { /** Returns true if the device should use the split notification shade. */ @Deprecated( - message = "This is deprecated, please use ShadeInteractor#shadeMode instead", + message = "This is deprecated, please use ShadeModeInteractor#shadeMode instead", replaceWith = ReplaceWith( - "shadeInteractor.shadeMode", - "com.android.systemui.shade.domain.interactor.ShadeInteractor", + "shadeModeInteractor.shadeMode", + "com.android.systemui.shade.domain.interactor.ShadeModeInteractor", ), ) fun shouldUseSplitNotificationShade(resources: Resources): Boolean diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt index 2fc22867e702..7879f971e193 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt @@ -16,7 +16,7 @@ package com.android.systemui.statusbar.policy.ui.dialog -import android.annotation.UiThread; +import android.annotation.UiThread import android.app.Dialog import android.content.Context import android.content.Intent @@ -44,6 +44,8 @@ import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.animation.Expandable 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.dialog.ui.composable.AlertDialogContent import com.android.systemui.plugins.ActivityStarter @@ -60,6 +62,8 @@ import com.android.systemui.util.Assert import javax.inject.Inject import javax.inject.Provider import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @SysUISingleton @@ -73,7 +77,9 @@ constructor( // Using a provider to avoid a circular dependency. private val viewModel: Provider<ModesDialogViewModel>, private val dialogEventLogger: ModesDialogEventLogger, + @Application private val applicationCoroutineScope: CoroutineScope, @Main private val mainCoroutineContext: CoroutineContext, + @Background private val bgContext: CoroutineContext, private val shadeDisplayContextRepository: ShadeDialogContextInteractor, ) : SystemUIDialog.Delegate { // NOTE: This should only be accessed/written from the main thread. @@ -185,6 +191,18 @@ constructor( * launches it normally without animating. */ fun launchFromDialog(intent: Intent) { + // TODO: b/394571336 - Remove this method and inline "actual" if b/394571336 fixed. + // Workaround for Compose bug, see b/394241061 and b/394571336 -- Need to post on the main + // thread so that dialog dismissal doesn't crash after a long press inside it (the *double* + // jump, out and back in, is because mainCoroutineContext is .immediate). + applicationCoroutineScope.launch { + withContext(bgContext) { + withContext(mainCoroutineContext) { actualLaunchFromDialog(intent) } + } + } + } + + private fun actualLaunchFromDialog(intent: Intent) { Assert.isMainThread() if (currentDialog == null) { Log.w( diff --git a/packages/SystemUI/src/com/android/systemui/stylus/OWNERS b/packages/SystemUI/src/com/android/systemui/stylus/OWNERS index 0ec996be72de..9b4902a9e7b2 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/OWNERS +++ b/packages/SystemUI/src/com/android/systemui/stylus/OWNERS @@ -6,5 +6,4 @@ madym@google.com mgalhardo@google.com petrcermak@google.com stevenckng@google.com -tkachenkoi@google.com -vanjan@google.com
\ No newline at end of file +vanjan@google.com diff --git a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt index f5aac720fd47..e1640cd4ce7a 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt @@ -19,8 +19,11 @@ package com.android.systemui.unfold import android.content.Context import android.hardware.devicestate.DeviceStateManager import android.util.Log +import androidx.annotation.VisibleForTesting import com.android.app.tracing.TraceUtils.traceAsync import com.android.app.tracing.instantForTrack +import com.android.internal.util.LatencyTracker +import com.android.internal.util.LatencyTracker.ACTION_SWITCH_DISPLAY_UNFOLD import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -30,10 +33,12 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.shared.model.ScreenPowerState import com.android.systemui.power.shared.model.WakeSleepReason +import com.android.systemui.power.shared.model.WakefulnessModel import com.android.systemui.power.shared.model.WakefulnessState import com.android.systemui.shared.system.SysUiStatsLog import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg +import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionStarted import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor import com.android.systemui.util.Compile import com.android.systemui.util.Utils.isDeviceFoldable @@ -42,17 +47,23 @@ import com.android.systemui.util.kotlin.pairwise import com.android.systemui.util.kotlin.race import com.android.systemui.util.time.SystemClock import com.android.systemui.util.time.measureTimeMillis -import java.time.Duration import java.util.concurrent.Executor import javax.inject.Inject +import kotlin.coroutines.cancellation.CancellationException +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flow -import com.android.app.tracing.coroutines.launchTraced as launch +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.timeout +import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout /** @@ -73,63 +84,96 @@ constructor( @Application private val applicationScope: CoroutineScope, private val displaySwitchLatencyLogger: DisplaySwitchLatencyLogger, private val systemClock: SystemClock, - private val deviceStateManager: DeviceStateManager + private val deviceStateManager: DeviceStateManager, + private val latencyTracker: LatencyTracker, ) : CoreStartable { private val backgroundDispatcher = singleThreadBgExecutor.asCoroutineDispatcher() private val isAodEnabled: Boolean get() = keyguardInteractor.isAodAvailable.value + private val displaySwitchStarted = + deviceStateRepository.state.pairwise().filter { + // Start tracking only when the foldable device is + // folding(UNFOLDED/HALF_FOLDED -> FOLDED) or unfolding(FOLDED -> HALF_FOLD/UNFOLDED) + foldableDeviceState -> + foldableDeviceState.previousValue == DeviceState.FOLDED || + foldableDeviceState.newValue == DeviceState.FOLDED + } + + private var startOrEndEvent: Flow<Any> = merge(displaySwitchStarted, anyEndEventFlow()) + + private var isCoolingDown = false + override fun start() { if (!isDeviceFoldable(context.resources, deviceStateManager)) { return } applicationScope.launch(context = backgroundDispatcher) { - deviceStateRepository.state - .pairwise() - .filter { - // Start tracking only when the foldable device is - // folding(UNFOLDED/HALF_FOLDED -> FOLDED) or - // unfolding(FOLDED -> HALF_FOLD/UNFOLDED) - foldableDeviceState -> - foldableDeviceState.previousValue == DeviceState.FOLDED || - foldableDeviceState.newValue == DeviceState.FOLDED + displaySwitchStarted.collectLatest { (previousState, newState) -> + if (isCoolingDown) return@collectLatest + if (previousState == DeviceState.FOLDED) { + latencyTracker.onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + instantForTrack(TAG) { "unfold latency tracking started" } } - .flatMapLatest { foldableDeviceState -> - flow { - var displaySwitchLatencyEvent = DisplaySwitchLatencyEvent() - val toFoldableDeviceState = foldableDeviceState.newValue.toStatsInt() - displaySwitchLatencyEvent = - displaySwitchLatencyEvent.withBeforeFields( - foldableDeviceState.previousValue.toStatsInt() - ) - + try { + withTimeout(SCREEN_EVENT_TIMEOUT) { + val event = + DisplaySwitchLatencyEvent().withBeforeFields(previousState.toStatsInt()) val displaySwitchTimeMs = measureTimeMillis(systemClock) { - try { - withTimeout(SCREEN_EVENT_TIMEOUT) { - traceAsync(TAG, "displaySwitch") { - waitForDisplaySwitch(toFoldableDeviceState) - } - } - } catch (e: TimeoutCancellationException) { - Log.e(TAG, "Wait for display switch timed out") + traceAsync(TAG, "displaySwitch") { + waitForDisplaySwitch(newState.toStatsInt()) } } - - displaySwitchLatencyEvent = - displaySwitchLatencyEvent.withAfterFields( - toFoldableDeviceState, - displaySwitchTimeMs.toInt(), - getCurrentState() - ) - emit(displaySwitchLatencyEvent) + if (previousState == DeviceState.FOLDED) { + latencyTracker.onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + logDisplaySwitchEvent(event, newState, displaySwitchTimeMs) } + } catch (e: TimeoutCancellationException) { + instantForTrack(TAG) { "tracking timed out" } + latencyTracker.onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD) + } catch (e: CancellationException) { + instantForTrack(TAG) { "new state interrupted, entering cool down" } + latencyTracker.onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD) + startCoolDown() } - .collect { displaySwitchLatencyLogger.log(it) } + } } } + @OptIn(FlowPreview::class) + private fun startCoolDown() { + if (isCoolingDown) return + isCoolingDown = true + applicationScope.launch(context = backgroundDispatcher) { + val startTime = systemClock.elapsedRealtime() + try { + startOrEndEvent.timeout(COOL_DOWN_DURATION).collect() + } catch (e: TimeoutCancellationException) { + instantForTrack(TAG) { + "cool down finished, lasted ${systemClock.elapsedRealtime() - startTime} ms" + } + isCoolingDown = false + } + } + } + + private fun logDisplaySwitchEvent( + event: DisplaySwitchLatencyEvent, + toFoldableDeviceState: DeviceState, + displaySwitchTimeMs: Long, + ) { + displaySwitchLatencyLogger.log( + event.withAfterFields( + toFoldableDeviceState.toStatsInt(), + displaySwitchTimeMs.toInt(), + getCurrentState(), + ) + ) + } + private fun DeviceState.toStatsInt(): Int = when (this) { DeviceState.FOLDED -> FOLDABLE_DEVICE_STATE_CLOSED @@ -152,25 +196,42 @@ constructor( } } + private fun anyEndEventFlow(): Flow<Any> { + val unfoldStatus = + unfoldTransitionInteractor.unfoldTransitionStatus.filter { it is TransitionStarted } + // dropping first emission as we're only interested in new emissions, not current state + val screenOn = + powerInteractor.screenPowerState.drop(1).filter { it == ScreenPowerState.SCREEN_ON } + val goToSleep = + powerInteractor.detailedWakefulness.drop(1).filter { sleepWithScreenOff(it) } + return merge(screenOn, goToSleep, unfoldStatus) + } + private fun shouldWaitForTransitionStart( toFoldableDeviceState: Int, - isTransitionEnabled: Boolean + isTransitionEnabled: Boolean, ): Boolean = (toFoldableDeviceState != FOLDABLE_DEVICE_STATE_CLOSED && isTransitionEnabled) private suspend fun waitForScreenTurnedOn() { traceAsync(TAG, "waitForScreenTurnedOn()") { - powerInteractor.screenPowerState.filter { it == ScreenPowerState.SCREEN_ON }.first() + // dropping first as it's stateFlow and will always emit latest value but we're + // only interested in new states + powerInteractor.screenPowerState + .drop(1) + .filter { it == ScreenPowerState.SCREEN_ON } + .first() } } private suspend fun waitForGoToSleepWithScreenOff() { traceAsync(TAG, "waitForGoToSleepWithScreenOff()") { - powerInteractor.detailedWakefulness - .filter { it.internalWakefulnessState == WakefulnessState.ASLEEP && !isAodEnabled } - .first() + powerInteractor.detailedWakefulness.filter { sleepWithScreenOff(it) }.first() } } + private fun sleepWithScreenOff(model: WakefulnessModel) = + model.internalWakefulnessState == WakefulnessState.ASLEEP && !isAodEnabled + private fun getCurrentState(): Int = when { isStateAod() -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__AOD @@ -205,7 +266,7 @@ constructor( private fun DisplaySwitchLatencyEvent.withAfterFields( toFoldableDeviceState: Int, displaySwitchTimeMs: Int, - toState: Int + toState: Int, ): DisplaySwitchLatencyEvent { log { "toFoldableDeviceState=$toFoldableDeviceState, " + @@ -217,7 +278,7 @@ constructor( return copy( toFoldableDeviceState = toFoldableDeviceState, latencyMs = displaySwitchTimeMs, - toState = toState + toState = toState, ) } @@ -250,14 +311,15 @@ constructor( val hallSensorToFirstHingeAngleChangeMs: Int = VALUE_UNKNOWN, val hallSensorToDeviceStateChangeMs: Int = VALUE_UNKNOWN, val onScreenTurningOnToOnDrawnMs: Int = VALUE_UNKNOWN, - val onDrawnToOnScreenTurnedOnMs: Int = VALUE_UNKNOWN + val onDrawnToOnScreenTurnedOnMs: Int = VALUE_UNKNOWN, ) companion object { private const val VALUE_UNKNOWN = -1 private const val TAG = "DisplaySwitchLatency" private val DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE) - private val SCREEN_EVENT_TIMEOUT = Duration.ofMillis(15000).toMillis() + @VisibleForTesting val SCREEN_EVENT_TIMEOUT = 15.seconds + @VisibleForTesting val COOL_DOWN_DURATION = 2.seconds private const val FOLDABLE_DEVICE_STATE_UNKNOWN = SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_UNKNOWN diff --git a/packages/SystemUI/src/com/android/systemui/unfold/NoCooldownDisplaySwitchLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/NoCooldownDisplaySwitchLatencyTracker.kt new file mode 100644 index 000000000000..91f142646c3d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/unfold/NoCooldownDisplaySwitchLatencyTracker.kt @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.unfold + +import android.content.Context +import android.hardware.devicestate.DeviceStateManager +import android.util.Log +import com.android.app.tracing.TraceUtils.traceAsync +import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.app.tracing.instantForTrack +import com.android.systemui.CoreStartable +import com.android.systemui.Flags.unfoldLatencyTrackingFix +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.display.data.repository.DeviceStateRepository +import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.power.shared.model.ScreenPowerState +import com.android.systemui.power.shared.model.WakeSleepReason +import com.android.systemui.power.shared.model.WakefulnessState +import com.android.systemui.shared.system.SysUiStatsLog +import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent +import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg +import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor +import com.android.systemui.util.Compile +import com.android.systemui.util.Utils.isDeviceFoldable +import com.android.systemui.util.animation.data.repository.AnimationStatusRepository +import com.android.systemui.util.kotlin.pairwise +import com.android.systemui.util.kotlin.race +import com.android.systemui.util.time.SystemClock +import com.android.systemui.util.time.measureTimeMillis +import java.time.Duration +import java.util.concurrent.Executor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.withTimeout + +/** + * Old version of [DisplaySwitchLatencyTracker] tracking only [DisplaySwitchLatencyEvent]. Which + * version is used for tracking depends on [unfoldLatencyTrackingFix] flag. + */ +@SysUISingleton +class NoCooldownDisplaySwitchLatencyTracker +@Inject +constructor( + private val context: Context, + private val deviceStateRepository: DeviceStateRepository, + private val powerInteractor: PowerInteractor, + private val unfoldTransitionInteractor: UnfoldTransitionInteractor, + private val animationStatusRepository: AnimationStatusRepository, + private val keyguardInteractor: KeyguardInteractor, + @UnfoldSingleThreadBg private val singleThreadBgExecutor: Executor, + @Application private val applicationScope: CoroutineScope, + private val displaySwitchLatencyLogger: DisplaySwitchLatencyLogger, + private val systemClock: SystemClock, + private val deviceStateManager: DeviceStateManager, +) : CoreStartable { + + private val backgroundDispatcher = singleThreadBgExecutor.asCoroutineDispatcher() + private val isAodEnabled: Boolean + get() = keyguardInteractor.isAodAvailable.value + + override fun start() { + if (!isDeviceFoldable(context.resources, deviceStateManager)) { + return + } + applicationScope.launch(context = backgroundDispatcher) { + deviceStateRepository.state + .pairwise() + .filter { + // Start tracking only when the foldable device is + // folding(UNFOLDED/HALF_FOLDED -> FOLDED) or + // unfolding(FOLDED -> HALF_FOLD/UNFOLDED) + foldableDeviceState -> + foldableDeviceState.previousValue == DeviceState.FOLDED || + foldableDeviceState.newValue == DeviceState.FOLDED + } + .flatMapLatest { foldableDeviceState -> + flow { + var displaySwitchLatencyEvent = DisplaySwitchLatencyEvent() + val toFoldableDeviceState = foldableDeviceState.newValue.toStatsInt() + displaySwitchLatencyEvent = + displaySwitchLatencyEvent.withBeforeFields( + foldableDeviceState.previousValue.toStatsInt() + ) + + val displaySwitchTimeMs = + measureTimeMillis(systemClock) { + try { + withTimeout(SCREEN_EVENT_TIMEOUT) { + traceAsync(TAG, "displaySwitch") { + waitForDisplaySwitch(toFoldableDeviceState) + } + } + } catch (e: TimeoutCancellationException) { + Log.e(TAG, "Wait for display switch timed out") + } + } + + displaySwitchLatencyEvent = + displaySwitchLatencyEvent.withAfterFields( + toFoldableDeviceState, + displaySwitchTimeMs.toInt(), + getCurrentState(), + ) + emit(displaySwitchLatencyEvent) + } + } + .collect { displaySwitchLatencyLogger.log(it) } + } + } + + private fun DeviceState.toStatsInt(): Int = + when (this) { + DeviceState.FOLDED -> FOLDABLE_DEVICE_STATE_CLOSED + DeviceState.HALF_FOLDED -> FOLDABLE_DEVICE_STATE_HALF_OPEN + DeviceState.UNFOLDED -> FOLDABLE_DEVICE_STATE_OPEN + DeviceState.CONCURRENT_DISPLAY -> FOLDABLE_DEVICE_STATE_FLIPPED + else -> FOLDABLE_DEVICE_STATE_UNKNOWN + } + + private suspend fun waitForDisplaySwitch(toFoldableDeviceState: Int) { + val isTransitionEnabled = + unfoldTransitionInteractor.isAvailable && + animationStatusRepository.areAnimationsEnabled().first() + if (shouldWaitForTransitionStart(toFoldableDeviceState, isTransitionEnabled)) { + traceAsync(TAG, "waitForTransitionStart()") { + unfoldTransitionInteractor.waitForTransitionStart() + } + } else { + race({ waitForScreenTurnedOn() }, { waitForGoToSleepWithScreenOff() }) + } + } + + private fun shouldWaitForTransitionStart( + toFoldableDeviceState: Int, + isTransitionEnabled: Boolean, + ): Boolean = (toFoldableDeviceState != FOLDABLE_DEVICE_STATE_CLOSED && isTransitionEnabled) + + private suspend fun waitForScreenTurnedOn() { + traceAsync(TAG, "waitForScreenTurnedOn()") { + // dropping first as it's stateFlow and will always emit latest value but we're + // only interested in new states + powerInteractor.screenPowerState + .drop(1) + .filter { it == ScreenPowerState.SCREEN_ON } + .first() + } + } + + private suspend fun waitForGoToSleepWithScreenOff() { + traceAsync(TAG, "waitForGoToSleepWithScreenOff()") { + powerInteractor.detailedWakefulness + .filter { it.internalWakefulnessState == WakefulnessState.ASLEEP && !isAodEnabled } + .first() + } + } + + private fun getCurrentState(): Int = + when { + isStateAod() -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__AOD + isStateScreenOff() -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__SCREEN_OFF + else -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__UNKNOWN + } + + private fun isStateAod(): Boolean = (isAsleepDueToFold() && isAodEnabled) + + private fun isStateScreenOff(): Boolean = (isAsleepDueToFold() && !isAodEnabled) + + private fun isAsleepDueToFold(): Boolean { + val lastWakefulnessEvent = powerInteractor.detailedWakefulness.value + + return (lastWakefulnessEvent.isAsleep() && + (lastWakefulnessEvent.lastSleepReason == WakeSleepReason.FOLD)) + } + + private inline fun log(msg: () -> String) { + if (DEBUG) Log.d(TAG, msg()) + } + + private fun DisplaySwitchLatencyEvent.withBeforeFields( + fromFoldableDeviceState: Int + ): DisplaySwitchLatencyEvent { + log { "fromFoldableDeviceState=$fromFoldableDeviceState" } + instantForTrack(TAG) { "fromFoldableDeviceState=$fromFoldableDeviceState" } + + return copy(fromFoldableDeviceState = fromFoldableDeviceState) + } + + private fun DisplaySwitchLatencyEvent.withAfterFields( + toFoldableDeviceState: Int, + displaySwitchTimeMs: Int, + toState: Int, + ): DisplaySwitchLatencyEvent { + log { + "toFoldableDeviceState=$toFoldableDeviceState, " + + "toState=$toState, " + + "latencyMs=$displaySwitchTimeMs" + } + instantForTrack(TAG) { "toFoldableDeviceState=$toFoldableDeviceState, toState=$toState" } + + return copy( + toFoldableDeviceState = toFoldableDeviceState, + latencyMs = displaySwitchTimeMs, + toState = toState, + ) + } + + companion object { + private const val VALUE_UNKNOWN = -1 + private const val TAG = "DisplaySwitchLatency" + private val DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE) + private val SCREEN_EVENT_TIMEOUT = Duration.ofMillis(15000).toMillis() + + private const val FOLDABLE_DEVICE_STATE_UNKNOWN = + SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_UNKNOWN + const val FOLDABLE_DEVICE_STATE_CLOSED = + SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_CLOSED + const val FOLDABLE_DEVICE_STATE_HALF_OPEN = + SysUiStatsLog + .DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_HALF_OPENED + private const val FOLDABLE_DEVICE_STATE_OPEN = + SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_OPENED + private const val FOLDABLE_DEVICE_STATE_FLIPPED = + SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_FLIPPED + } +} diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt index f806a5c52d5a..9248cc801227 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt @@ -22,6 +22,7 @@ import android.hardware.devicestate.DeviceStateManager import android.os.Trace import android.util.Log import com.android.internal.util.LatencyTracker +import com.android.systemui.Flags.unfoldLatencyTrackingFix import com.android.systemui.dagger.qualifiers.UiBackground import com.android.systemui.keyguard.ScreenLifecycle import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener @@ -63,7 +64,7 @@ constructor( /** Registers for relevant events only if the device is foldable. */ fun init() { - if (!isFoldable) { + if (unfoldLatencyTrackingFix() || !isFoldable) { return } deviceStateManager.registerCallback(uiBgExecutor, foldStateListener) @@ -85,7 +86,7 @@ constructor( if (DEBUG) { Log.d( TAG, - "onScreenTurnedOn: folded = $folded, isTransitionEnabled = $isTransitionEnabled" + "onScreenTurnedOn: folded = $folded, isTransitionEnabled = $isTransitionEnabled", ) } @@ -109,7 +110,7 @@ constructor( if (DEBUG) { Log.d( TAG, - "onTransitionStarted: folded = $folded, isTransitionEnabled = $isTransitionEnabled" + "onTransitionStarted: folded = $folded, isTransitionEnabled = $isTransitionEnabled", ) } @@ -161,7 +162,7 @@ constructor( Log.d( TAG, "Starting ACTION_SWITCH_DISPLAY_UNFOLD, " + - "isTransitionEnabled = $isTransitionEnabled" + "isTransitionEnabled = $isTransitionEnabled", ) } } diff --git a/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt index 885a2b0d7305..c2f86a37c6d8 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt @@ -21,6 +21,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.res.R import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.unfold.data.repository.UnfoldTransitionRepository +import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionFinished import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionInProgress import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionStarted @@ -48,6 +49,9 @@ constructor( val isAvailable: Boolean get() = repository.isAvailable + /** Flow of latest [UnfoldTransitionStatus] changes */ + val unfoldTransitionStatus: Flow<UnfoldTransitionStatus> = repository.transitionStatus + /** * This mapping emits 1 when the device is completely unfolded and 0.0 when the device is * completely folded. diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt index ad97b21ea60b..c960b5525d96 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt @@ -21,6 +21,7 @@ import android.annotation.SuppressLint import android.annotation.UserIdInt import android.app.admin.DevicePolicyManager import android.content.Context +import android.content.Intent import android.content.IntentFilter import android.content.pm.UserInfo import android.content.res.Resources @@ -84,6 +85,9 @@ interface UserRepository { /** [UserInfo] of the currently-selected user. */ val selectedUserInfo: Flow<UserInfo> + /** Tracks whether the main user is unlocked. */ + fun isUserUnlocked(userHandle: UserHandle?): Flow<Boolean> + /** User ID of the main user. */ val mainUserId: Int @@ -284,6 +288,18 @@ constructor( } .stateIn(applicationScope, SharingStarted.Eagerly, false) + override fun isUserUnlocked(userHandle: UserHandle?): Flow<Boolean> = + broadcastDispatcher + .broadcastFlow(IntentFilter(Intent.ACTION_USER_UNLOCKED)) + .map { getUnlockedState(userHandle) } + .onStart { emit(getUnlockedState(userHandle)) } + + private suspend fun getUnlockedState(userHandle: UserHandle?): Boolean { + return withContext(backgroundDispatcher) { + userHandle?.let { user -> manager.isUserUnlocked(user) } ?: false + } + } + @SuppressLint("MissingPermission") override val isLogoutToSystemUserEnabled: StateFlow<Boolean> = selectedUser diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt new file mode 100644 index 000000000000..ef29a387e06f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.user.domain.interactor + +import android.os.UserHandle +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.user.data.repository.UserRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow + +@SysUISingleton +class UserLockedInteractor @Inject constructor(val userRepository: UserRepository) { + fun isUserUnlocked(userHandle: UserHandle?): Flow<Boolean> = + userRepository.isUserUnlocked(userHandle) +} diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt index 7e7527ea4be3..735da46667c5 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt @@ -16,6 +16,7 @@ package com.android.systemui.util.kotlin +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.util.time.SystemClock import com.android.systemui.util.time.SystemClockImpl import java.util.concurrent.atomic.AtomicReference @@ -33,7 +34,6 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart -import com.android.app.tracing.coroutines.launchTraced as launch /** * Returns a new [Flow] that combines the two most recent emissions from [this] using [transform]. @@ -246,24 +246,24 @@ fun <T> Flow<T>.throttle(periodMs: Long, clock: SystemClock = SystemClockImpl()) } inline fun <T1, T2, T3, T4, T5, T6, R> combine( - flow: Flow<T1>, - flow2: Flow<T2>, - flow3: Flow<T3>, - flow4: Flow<T4>, - flow5: Flow<T5>, - flow6: Flow<T6>, - crossinline transform: suspend (T1, T2, T3, T4, T5, T6) -> R + flow: Flow<T1>, + flow2: Flow<T2>, + flow3: Flow<T3>, + flow4: Flow<T4>, + flow5: Flow<T5>, + flow6: Flow<T6>, + crossinline transform: suspend (T1, T2, T3, T4, T5, T6) -> R, ): Flow<R> { - return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6) { - args: Array<*> -> + return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6) { args: Array<*> + -> @Suppress("UNCHECKED_CAST") transform( - args[0] as T1, - args[1] as T2, - args[2] as T3, - args[3] as T4, - args[4] as T5, - args[5] as T6, + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, ) } } @@ -276,7 +276,7 @@ inline fun <T1, T2, T3, T4, T5, T6, T7, R> combine( flow5: Flow<T5>, flow6: Flow<T6>, flow7: Flow<T7>, - crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R + crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R, ): Flow<R> { return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6, flow7) { args: Array<*> -> @@ -288,7 +288,7 @@ inline fun <T1, T2, T3, T4, T5, T6, T7, R> combine( args[3] as T4, args[4] as T5, args[5] as T6, - args[6] as T7 + args[6] as T7, ) } } @@ -302,7 +302,7 @@ inline fun <T1, T2, T3, T4, T5, T6, T7, T8, R> combine( flow6: Flow<T6>, flow7: Flow<T7>, flow8: Flow<T8>, - crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8) -> R + crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8) -> R, ): Flow<R> { return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8) { args: Array<*> -> @@ -315,7 +315,7 @@ inline fun <T1, T2, T3, T4, T5, T6, T7, T8, R> combine( args[4] as T5, args[5] as T6, args[6] as T7, - args[7] as T8 + args[7] as T8, ) } } @@ -330,7 +330,7 @@ inline fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, R> combine( flow7: Flow<T7>, flow8: Flow<T8>, flow9: Flow<T9>, - crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8, T9) -> R + crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8, T9) -> R, ): Flow<R> { return kotlinx.coroutines.flow.combine( flow, @@ -341,7 +341,7 @@ inline fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, R> combine( flow6, flow7, flow8, - flow9 + flow9, ) { args: Array<*> -> @Suppress("UNCHECKED_CAST") transform( @@ -352,8 +352,8 @@ inline fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, R> combine( args[4] as T5, args[5] as T6, args[6] as T7, - args[6] as T8, - args[6] as T9, + args[7] as T8, + args[8] as T9, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSystemSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSystemSettingsRepository.kt index 4b01ded16495..f1abf105be8e 100644 --- a/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSystemSettingsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSystemSettingsRepository.kt @@ -25,7 +25,7 @@ import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineDispatcher /** - * Repository for observing values of [Settings.Secure] for the currently active user. That means + * Repository for observing values of [Settings.System] for the currently active user. That means * when user is switched and the new user has different value, flow will emit new value. */ // TODO: b/377244768 - Make internal once call sites inject SystemSettingsRepository instead. diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java index 68bffeefb0f0..4d5477052388 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java @@ -37,8 +37,6 @@ import android.media.IAudioService; import android.media.IVolumeController; import android.media.MediaRouter2Manager; import android.media.VolumePolicy; -import android.media.session.MediaController.PlaybackInfo; -import android.media.session.MediaSession.Token; import android.net.Uri; import android.os.Handler; import android.os.HandlerExecutor; @@ -61,6 +59,7 @@ import androidx.lifecycle.Observer; import com.android.internal.annotations.GuardedBy; import com.android.settingslib.volume.MediaSessions; +import com.android.settingslib.volume.MediaSessions.SessionId; import com.android.systemui.Dumpable; import com.android.systemui.Flags; import com.android.systemui.broadcast.BroadcastDispatcher; @@ -1402,12 +1401,13 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa } protected final class MediaSessionsCallbacks implements MediaSessions.Callbacks { - private final HashMap<Token, Integer> mRemoteStreams = new HashMap<>(); + private final HashMap<SessionId, Integer> mRemoteStreams = new HashMap<>(); private int mNextStream = DYNAMIC_STREAM_REMOTE_START_INDEX; @Override - public void onRemoteUpdate(Token token, String name, PlaybackInfo pi) { + public void onRemoteUpdate( + SessionId token, String name, MediaSessions.VolumeInfo volumeInfo) { addStream(token, "onRemoteUpdate"); int stream = 0; @@ -1415,14 +1415,15 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa stream = mRemoteStreams.get(token); } Slog.d(TAG, - "onRemoteUpdate: stream: " + stream + " volume: " + pi.getCurrentVolume()); + "onRemoteUpdate: stream: " + + stream + " volume: " + volumeInfo.getCurrentVolume()); boolean changed = mState.states.indexOfKey(stream) < 0; final StreamState ss = streamStateW(stream); ss.dynamic = true; ss.levelMin = 0; - ss.levelMax = pi.getMaxVolume(); - if (ss.level != pi.getCurrentVolume()) { - ss.level = pi.getCurrentVolume(); + ss.levelMax = volumeInfo.getMaxVolume(); + if (ss.level != volumeInfo.getCurrentVolume()) { + ss.level = volumeInfo.getCurrentVolume(); changed = true; } if (!Objects.equals(ss.remoteLabel, name)) { @@ -1437,11 +1438,11 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa } @Override - public void onRemoteVolumeChanged(Token token, int flags) { - addStream(token, "onRemoteVolumeChanged"); + public void onRemoteVolumeChanged(SessionId sessionId, int flags) { + addStream(sessionId, "onRemoteVolumeChanged"); int stream = 0; synchronized (mRemoteStreams) { - stream = mRemoteStreams.get(token); + stream = mRemoteStreams.get(sessionId); } final boolean showUI = shouldShowUI(flags); Slog.d(TAG, "onRemoteVolumeChanged: stream: " + stream + " showui? " + showUI); @@ -1459,7 +1460,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa } @Override - public void onRemoteRemoved(Token token) { + public void onRemoteRemoved(SessionId token) { int stream; synchronized (mRemoteStreams) { if (!mRemoteStreams.containsKey(token)) { @@ -1480,7 +1481,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa } public void setStreamVolume(int stream, int level) { - final Token token = findToken(stream); + final SessionId token = findToken(stream); if (token == null) { Log.w(TAG, "setStreamVolume: No token found for stream: " + stream); return; @@ -1488,9 +1489,9 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa mMediaSessions.setVolume(token, level); } - private Token findToken(int stream) { + private SessionId findToken(int stream) { synchronized (mRemoteStreams) { - for (Map.Entry<Token, Integer> entry : mRemoteStreams.entrySet()) { + for (Map.Entry<SessionId, Integer> entry : mRemoteStreams.entrySet()) { if (entry.getValue().equals(stream)) { return entry.getKey(); } @@ -1499,7 +1500,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa return null; } - private void addStream(Token token, String triggeringMethod) { + private void addStream(SessionId token, String triggeringMethod) { synchronized (mRemoteStreams) { if (!mRemoteStreams.containsKey(token)) { mRemoteStreams.put(token, mNextStream); diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt index 83b7c1818341..86defff4a120 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt @@ -68,7 +68,7 @@ constructor( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.volume_dialog) - requireViewById<View>(R.id.volume_dialog_root).repeatWhenAttached { + requireViewById<View>(R.id.volume_dialog).repeatWhenAttached { coroutineScopeTraced("[Volume]dialog") { val component = componentFactory.create(this) with(component.volumeDialogViewBinder()) { bind(this@VolumeDialog) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt index 20a74b027db5..afe3d7bf217a 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt @@ -17,7 +17,9 @@ package com.android.systemui.volume.dialog.domain.interactor import android.annotation.SuppressLint +import android.provider.Settings import com.android.systemui.plugins.VolumeDialogController +import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository import com.android.systemui.volume.Events import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPlugin import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope @@ -28,8 +30,9 @@ import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityMod import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel.Visible import com.android.systemui.volume.dialog.utils.VolumeTracer import javax.inject.Inject -import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds +import kotlin.time.DurationUnit import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -43,8 +46,6 @@ import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn -private val MAX_DIALOG_SHOW_TIME: Duration = 3.seconds - /** * Handles Volume Dialog visibility state. It might change from several sources: * - [com.android.systemui.plugins.VolumeDialogController] requests visibility change; @@ -60,8 +61,11 @@ constructor( private val tracer: VolumeTracer, private val repository: VolumeDialogVisibilityRepository, private val controller: VolumeDialogController, + private val secureSettingsRepository: SecureSettingsRepository, ) { + private val defaultTimeout = 3.seconds + @SuppressLint("SharedFlowCreation") private val mutableDismissDialogEvents = MutableSharedFlow<Unit>(extraBufferCapacity = 1) val dialogVisibility: Flow<VolumeDialogVisibilityModel> = @@ -73,7 +77,14 @@ constructor( init { merge( mutableDismissDialogEvents.mapLatest { - delay(MAX_DIALOG_SHOW_TIME) + delay( + secureSettingsRepository + .getInt( + Settings.Secure.VOLUME_DIALOG_DISMISS_TIMEOUT, + defaultTimeout.toInt(DurationUnit.MILLISECONDS), + ) + .milliseconds + ) VolumeDialogEventModel.DismissRequested(Events.DISMISS_REASON_TIMEOUT) }, callbacksInteractor.event, diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt index 3d0c7d64b2a4..92ec4f554548 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt @@ -246,16 +246,12 @@ constructor( uiModel.drawerState.currentMode != uiModel.drawerState.previousMode ) { val count = uiModel.availableButtons.size - val selectedButton = - getChildAt(count - uiModel.currentButtonIndex) - .requireViewById<ImageButton>(R.id.volume_drawer_button) + val selectedButton = getChildAt(count - uiModel.currentButtonIndex) as ImageButton val previousIndex = uiModel.availableButtons.indexOfFirst { it.ringerMode == uiModel.drawerState.previousMode } - val unselectedButton = - getChildAt(count - previousIndex) - .requireViewById<ImageButton>(R.id.volume_drawer_button) + val unselectedButton = getChildAt(count - previousIndex) as ImageButton // We only need to execute on roundness animation end and volume dialog background // progress update once because these changes should be applied once on volume dialog // background and ringer drawer views. @@ -306,7 +302,7 @@ constructor( ) { val count = uiModel.availableButtons.size uiModel.availableButtons.fastForEachIndexed { index, ringerButton -> - val view = getChildAt(count - index) + val view = getChildAt(count - index) as ImageButton val isOpen = uiModel.drawerState is RingerDrawerState.Open if (index == uiModel.currentButtonIndex) { view.bindDrawerButton( @@ -323,37 +319,37 @@ constructor( onAnimationEnd?.run() } - private fun View.bindDrawerButton( + private fun ImageButton.bindDrawerButton( buttonViewModel: RingerButtonViewModel, viewModel: VolumeDialogRingerDrawerViewModel, isOpen: Boolean, isSelected: Boolean = false, isAnimated: Boolean = false, ) { + // id = buttonViewModel.viewId + setSelected(isSelected) val ringerContentDesc = context.getString(buttonViewModel.contentDescriptionResId) - with(requireViewById<ImageButton>(R.id.volume_drawer_button)) { - setImageResource(buttonViewModel.imageResId) - contentDescription = - if (isSelected && !isOpen) { - context.getString( - R.string.volume_ringer_drawer_closed_content_description, - ringerContentDesc, - ) - } else { - ringerContentDesc - } - if (isSelected && !isAnimated) { - setBackgroundResource(R.drawable.volume_drawer_selection_bg) - setColorFilter(context.getColor(internalR.color.materialColorOnPrimary)) - background = background.mutate() - } else if (!isAnimated) { - setBackgroundResource(R.drawable.volume_ringer_item_bg) - setColorFilter(context.getColor(internalR.color.materialColorOnSurface)) - background = background.mutate() - } - setOnClickListener { - viewModel.onRingerButtonClicked(buttonViewModel.ringerMode, isSelected) + setImageResource(buttonViewModel.imageResId) + contentDescription = + if (isSelected && !isOpen) { + context.getString( + R.string.volume_ringer_drawer_closed_content_description, + ringerContentDesc, + ) + } else { + ringerContentDesc } + if (isSelected && !isAnimated) { + setBackgroundResource(R.drawable.volume_drawer_selection_bg) + setColorFilter(context.getColor(internalR.color.materialColorOnPrimary)) + background = background.mutate() + } else if (!isAnimated) { + setBackgroundResource(R.drawable.volume_ringer_item_bg) + setColorFilter(context.getColor(internalR.color.materialColorOnSurface)) + background = background.mutate() + } + setOnClickListener { + viewModel.onRingerButtonClicked(buttonViewModel.ringerMode, isSelected) } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt index f2d7d956291c..7cc4bcc4e11c 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt @@ -74,7 +74,7 @@ constructor( val insets: MutableStateFlow<WindowInsets> = MutableStateFlow(WindowInsets.Builder().build()) // Root view of the Volume Dialog. - val root: MotionLayout = dialog.requireViewById(R.id.volume_dialog_root) + val root: MotionLayout = dialog.requireViewById(R.id.volume_dialog) animateVisibility(root, dialog, viewModel.dialogVisibilityModel) diff --git a/packages/SystemUI/src/com/android/systemui/window/dagger/WindowRootViewBlurModule.kt b/packages/SystemUI/src/com/android/systemui/window/dagger/WindowRootViewBlurModule.kt new file mode 100644 index 000000000000..95b3b68fa1ca --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/window/dagger/WindowRootViewBlurModule.kt @@ -0,0 +1,35 @@ +/* + * 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.window.dagger + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.window.data.repository.WindowRootViewBlurRepository +import com.android.systemui.window.data.repository.WindowRootViewBlurRepositoryImpl +import dagger.Binds +import dagger.Module + +/** + * Module that can be installed in sysui variants where we support cross window blur. + */ +@Module +interface WindowRootViewBlurModule { + @Binds + @SysUISingleton + fun bindWindowRootViewBlurRepository( + windowRootViewBlurRepositoryImpl: WindowRootViewBlurRepositoryImpl + ): WindowRootViewBlurRepository +} diff --git a/packages/SystemUI/src/com/android/systemui/window/dagger/WindowRootViewBlurNotSupportedModule.kt b/packages/SystemUI/src/com/android/systemui/window/dagger/WindowRootViewBlurNotSupportedModule.kt new file mode 100644 index 000000000000..ae917e072ff3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/window/dagger/WindowRootViewBlurNotSupportedModule.kt @@ -0,0 +1,35 @@ +/* + * 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.window.dagger + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.window.data.repository.NoopWindowRootViewBlurRepository +import com.android.systemui.window.data.repository.WindowRootViewBlurRepository +import dagger.Binds +import dagger.Module + +/** + * Module that can be installed in sysui variants where we don't support cross window blur. + */ +@Module +interface WindowRootViewBlurNotSupportedModule { + @Binds + @SysUISingleton + fun bindWindowRootViewBlurRepository( + windowRootViewBlurRepositoryImpl: NoopWindowRootViewBlurRepository + ): WindowRootViewBlurRepository +} diff --git a/packages/SystemUI/src/com/android/systemui/window/data/repository/NoopWindowRootViewBlurRepository.kt b/packages/SystemUI/src/com/android/systemui/window/data/repository/NoopWindowRootViewBlurRepository.kt new file mode 100644 index 000000000000..80aa11a71569 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/window/data/repository/NoopWindowRootViewBlurRepository.kt @@ -0,0 +1,28 @@ +/* + * 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.window.data.repository + +import com.android.systemui.window.data.repository.WindowRootViewBlurRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class NoopWindowRootViewBlurRepository @Inject constructor() : WindowRootViewBlurRepository { + override val blurRadius: MutableStateFlow<Int> = MutableStateFlow(0) + override val isBlurOpaque: MutableStateFlow<Boolean> = MutableStateFlow(true) + override val isBlurSupported: StateFlow<Boolean> = MutableStateFlow(false) +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt b/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt index 6b7de982e00a..41ceda033bdf 100644 --- a/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt @@ -16,18 +16,77 @@ package com.android.systemui.window.data.repository -import android.annotation.SuppressLint +import android.app.ActivityManager +import android.os.SystemProperties +import android.view.CrossWindowBlurListeners +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import com.android.systemui.window.data.repository.WindowRootViewBlurRepository.Companion.isDisableBlurSysPropSet +import java.util.concurrent.Executor import javax.inject.Inject -import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn /** Repository that maintains state for the window blur effect. */ +interface WindowRootViewBlurRepository { + val blurRadius: MutableStateFlow<Int> + val isBlurOpaque: MutableStateFlow<Boolean> + + /** Is blur supported based on settings toggle and battery power saver mode. */ + val isBlurSupported: StateFlow<Boolean> + + companion object { + /** + * Whether the `persist.sysui.disableBlur` is set, this is used to disable blur for tests. + */ + @JvmStatic + fun isDisableBlurSysPropSet() = SystemProperties.getBoolean(DISABLE_BLUR_PROPERTY, false) + + // property that can be used to disable the cross window blur for tests + private const val DISABLE_BLUR_PROPERTY = "persist.sysui.disableBlur" + } +} + @SysUISingleton -class WindowRootViewBlurRepository @Inject constructor() { - val blurRadius = MutableStateFlow(0) +class WindowRootViewBlurRepositoryImpl +@Inject +constructor( + crossWindowBlurListeners: CrossWindowBlurListeners, + @Main private val executor: Executor, + @Application private val scope: CoroutineScope, +) : WindowRootViewBlurRepository { + override val blurRadius = MutableStateFlow(0) + + override val isBlurOpaque = MutableStateFlow(false) + + override val isBlurSupported: StateFlow<Boolean> = + conflatedCallbackFlow { + val sendUpdate = { value: Boolean -> + trySendWithFailureLogging( + isBlurAllowed() && value, + TAG, + "unable to send blur enabled/disable state change", + ) + } + crossWindowBlurListeners.addListener(executor, sendUpdate) + sendUpdate(crossWindowBlurListeners.isCrossWindowBlurEnabled) + + awaitClose { crossWindowBlurListeners.removeListener(sendUpdate) } + } // stateIn because this is backed by a binder call. + .stateIn(scope, SharingStarted.WhileSubscribed(), false) - val isBlurOpaque = MutableStateFlow(false) + private fun isBlurAllowed(): Boolean { + return ActivityManager.isHighEndGfx() && !isDisableBlurSysPropSet() + } - @SuppressLint("SharedFlowCreation") val onBlurApplied = MutableSharedFlow<Int>() + companion object { + const val TAG = "WindowRootViewBlurRepository" + } } diff --git a/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt b/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt index e21e0a1cadc7..7a88a2ef966b 100644 --- a/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.window.domain.interactor +import android.annotation.SuppressLint import android.util.Log import com.android.systemui.Flags import com.android.systemui.communal.domain.interactor.CommunalInteractor @@ -28,6 +29,7 @@ import com.android.systemui.window.data.repository.WindowRootViewBlurRepository import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -52,6 +54,8 @@ constructor( private val communalInteractor: CommunalInteractor, private val repository: WindowRootViewBlurRepository, ) { + @SuppressLint("SharedFlowCreation") private val _onBlurAppliedEvent = MutableSharedFlow<Int>() + private var isBouncerTransitionInProgress: StateFlow<Boolean> = if (Flags.bouncerUiRevamp()) { keyguardTransitionInteractor @@ -68,16 +72,22 @@ constructor( * root view. */ suspend fun onBlurApplied(appliedBlurRadius: Int) { - repository.onBlurApplied.emit(appliedBlurRadius) + _onBlurAppliedEvent.emit(appliedBlurRadius) } + /** + * Whether blur is enabled or not based on settings toggle, critical thermal state, battery save + * state and multimedia tunneling state. + */ + val isBlurCurrentlySupported: StateFlow<Boolean> = repository.isBlurSupported + /** Radius of blur to be applied on the window root view. */ val blurRadius: StateFlow<Int> = repository.blurRadius.asStateFlow() /** * Emits the applied blur radius whenever blur is successfully applied to the window root view. */ - val onBlurAppliedEvent: Flow<Int> = repository.onBlurApplied + val onBlurAppliedEvent: Flow<Int> = _onBlurAppliedEvent /** Whether the blur applied is opaque or transparent. */ val isBlurOpaque: Flow<Boolean> = diff --git a/packages/SystemUI/tests/res/layout/custom_view_flipper.xml b/packages/SystemUI/tests/res/layout/custom_view_flipper.xml new file mode 100644 index 000000000000..eb3ba82b043b --- /dev/null +++ b/packages/SystemUI/tests/res/layout/custom_view_flipper.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <ViewFlipper + android:id="@+id/flipper" + android:layout_width="match_parent" + android:layout_height="400dp" + android:flipInterval="1000" + /> + +</FrameLayout>
\ No newline at end of file diff --git a/packages/SystemUI/tests/res/layout/custom_view_flipper_image.xml b/packages/SystemUI/tests/res/layout/custom_view_flipper_image.xml new file mode 100644 index 000000000000..e2a00bd845cd --- /dev/null +++ b/packages/SystemUI/tests/res/layout/custom_view_flipper_image.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<ImageView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/imageview" + android:layout_width="match_parent" + android:layout_height="400dp" />
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index e8054c07eac8..4ccfa29d4ba0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -206,7 +206,6 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { private @Mock ShadeInteractor mShadeInteractor; private @Mock ShadeWindowLogger mShadeWindowLogger; private @Mock SelectedUserInteractor mSelectedUserInteractor; - private @Mock UserTracker.Callback mUserTrackerCallback; private @Mock KeyguardInteractor mKeyguardInteractor; private @Mock KeyguardTransitionBootInteractor mKeyguardTransitionBootInteractor; private @Captor ArgumentCaptor<KeyguardStateController.Callback> @@ -281,7 +280,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { () -> mShadeInteractor, mShadeWindowLogger, () -> mSelectedUserInteractor, - mock(UserTracker.class), + mUserTracker, mKosmos.getNotificationShadeWindowModel(), mSecureSettings, mKosmos::getCommunalInteractor, @@ -319,7 +318,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { } catch (Exception e) { // Just so we don't have to add the exception signature to every test. - fail(e.getMessage()); + fail(); } } @@ -331,156 +330,18 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { /* First test the default behavior: handleUserSwitching() is not invoked */ when(mUserTracker.isUserSwitching()).thenReturn(false); + mViewMediator.mUpdateCallback = mock(KeyguardUpdateMonitorCallback.class); mViewMediator.onSystemReady(); TestableLooper.get(this).processAllMessages(); - verify(mUserTrackerCallback, never()).onUserChanging(eq(userId), eq(mContext), - any(Runnable.class)); + verify(mViewMediator.mUpdateCallback, never()).onUserSwitching(userId); /* Next test user switching is already in progress when started */ when(mUserTracker.isUserSwitching()).thenReturn(true); mViewMediator.onSystemReady(); TestableLooper.get(this).processAllMessages(); - verify(mUserTrackerCallback).onUserChanging(eq(userId), eq(mContext), - any(Runnable.class)); - } - - @Test - @TestableLooper.RunWithLooper(setAsMainLooper = true) - public void testGoingAwayFollowedByBeforeUserSwitchDoesNotHideKeyguard() { - setCurrentUser(/* userId= */1099, /* isSecure= */false); - - // Setup keyguard - mViewMediator.onSystemReady(); - processAllMessagesAndBgExecutorMessages(); - mViewMediator.setShowingLocked(true, ""); - - // Request keyguard going away - when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(true); - mViewMediator.mKeyguardGoingAwayRunnable.run(); - - // After the request, begin a switch to a new secure user - int nextUserId = 500; - setCurrentUser(nextUserId, /* isSecure= */true); - Runnable result = mock(Runnable.class); - mViewMediator.handleBeforeUserSwitching(nextUserId, result); - processAllMessagesAndBgExecutorMessages(); - verify(result).run(); - - // After that request has begun, have WM tell us to exit keyguard - RemoteAnimationTarget[] apps = new RemoteAnimationTarget[]{ - mock(RemoteAnimationTarget.class) - }; - RemoteAnimationTarget[] wallpapers = new RemoteAnimationTarget[]{ - mock(RemoteAnimationTarget.class) - }; - IRemoteAnimationFinishedCallback callback = mock(IRemoteAnimationFinishedCallback.class); - mViewMediator.startKeyguardExitAnimation(TRANSIT_OLD_KEYGUARD_GOING_AWAY, apps, wallpapers, - null, callback); - processAllMessagesAndBgExecutorMessages(); - - // The call to exit should be rejected, and keyguard should still be visible - verify(mKeyguardUnlockAnimationController, never()).notifyStartSurfaceBehindRemoteAnimation( - any(), any(), any(), anyLong(), anyBoolean()); - try { - assertATMSLockScreenShowing(true); - } catch (Exception e) { - fail(e.getMessage()); - } - assertTrue(mViewMediator.isShowingAndNotOccluded()); - } - - @Test - @TestableLooper.RunWithLooper(setAsMainLooper = true) - public void testUserSwitchToSecureUserShowsBouncer() { - setCurrentUser(/* userId= */1099, /* isSecure= */true); - - // Setup keyguard - mViewMediator.onSystemReady(); - processAllMessagesAndBgExecutorMessages(); - mViewMediator.setShowingLocked(true, ""); - - // After the request, begin a switch to a new secure user - int nextUserId = 500; - setCurrentUser(nextUserId, /* isSecure= */true); - - Runnable beforeResult = mock(Runnable.class); - mViewMediator.handleBeforeUserSwitching(nextUserId, beforeResult); - processAllMessagesAndBgExecutorMessages(); - verify(beforeResult).run(); - - // Dismiss should not be called while user switch is in progress - Runnable onSwitchResult = mock(Runnable.class); - mViewMediator.handleUserSwitching(nextUserId, onSwitchResult); - processAllMessagesAndBgExecutorMessages(); - verify(onSwitchResult).run(); - verify(mStatusBarKeyguardViewManager, never()).dismissAndCollapse(); - - // The attempt to dismiss only comes on user switch complete, which will trigger a call to - // show the bouncer in StatusBarKeyguardViewManager - mViewMediator.handleUserSwitchComplete(nextUserId); - TestableLooper.get(this).moveTimeForward(600); - processAllMessagesAndBgExecutorMessages(); - - verify(mStatusBarKeyguardViewManager).dismissAndCollapse(); - } - - @Test - @TestableLooper.RunWithLooper(setAsMainLooper = true) - public void testUserSwitchToInsecureUserDismissesKeyguard() { - int userId = 1099; - when(mUserTracker.getUserId()).thenReturn(userId); - - // Setup keyguard - mViewMediator.onSystemReady(); - processAllMessagesAndBgExecutorMessages(); - mViewMediator.setShowingLocked(true, ""); - - // After the request, begin a switch to an insecure user - int nextUserId = 500; - when(mLockPatternUtils.isSecure(nextUserId)).thenReturn(false); - - Runnable beforeResult = mock(Runnable.class); - mViewMediator.handleBeforeUserSwitching(nextUserId, beforeResult); - processAllMessagesAndBgExecutorMessages(); - verify(beforeResult).run(); - - // The call to dismiss comes during the user switch - Runnable onSwitchResult = mock(Runnable.class); - mViewMediator.handleUserSwitching(nextUserId, onSwitchResult); - processAllMessagesAndBgExecutorMessages(); - verify(onSwitchResult).run(); - - verify(mStatusBarKeyguardViewManager).dismissAndCollapse(); - } - - @Test - @TestableLooper.RunWithLooper(setAsMainLooper = true) - public void testUserSwitchToSecureUserWhileKeyguardNotVisibleShowsKeyguard() { - setCurrentUser(/* userId= */1099, /* isSecure= */true); - - // Setup keyguard as not visible - mViewMediator.onSystemReady(); - processAllMessagesAndBgExecutorMessages(); - mViewMediator.setShowingLocked(false, ""); - processAllMessagesAndBgExecutorMessages(); - - // Begin a switch to a new secure user - int nextUserId = 500; - setCurrentUser(nextUserId, /* isSecure= */true); - - Runnable beforeResult = mock(Runnable.class); - mViewMediator.handleBeforeUserSwitching(nextUserId, beforeResult); - processAllMessagesAndBgExecutorMessages(); - verify(beforeResult).run(); - - try { - assertATMSLockScreenShowing(true); - } catch (Exception e) { - fail(); - } - assertTrue(mViewMediator.isShowingAndNotOccluded()); + verify(mViewMediator.mUpdateCallback).onUserSwitching(userId); } @Test @@ -1244,7 +1105,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { processAllMessagesAndBgExecutorMessages(); verify(mStatusBarKeyguardViewManager, never()).reset(anyBoolean()); - + assertATMSAndKeyguardViewMediatorStatesMatch(); } @Test @@ -1288,7 +1149,6 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { IRemoteAnimationFinishedCallback callback = mock(IRemoteAnimationFinishedCallback.class); when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(true); - mViewMediator.mKeyguardGoingAwayRunnable.run(); mViewMediator.startKeyguardExitAnimation(TRANSIT_OLD_KEYGUARD_GOING_AWAY, apps, wallpapers, null, callback); processAllMessagesAndBgExecutorMessages(); @@ -1343,6 +1203,13 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { // The captor will have the most recent setLockScreenShown call's value. assertEquals(showing, showingCaptor.getValue()); + + // We're now just after the last setLockScreenShown call. If we expect the lockscreen to be + // showing, ensure that we didn't subsequently ask for it to go away. + if (showing) { + orderedSetLockScreenShownCalls.verify(mActivityTaskManagerService, never()) + .keyguardGoingAway(anyInt()); + } } /** @@ -1504,7 +1371,6 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mKeyguardTransitionBootInteractor, mKosmos::getCommunalSceneInteractor, mock(WindowManagerOcclusionManager.class)); - mViewMediator.mUserChangedCallback = mUserTrackerCallback; mViewMediator.start(); mViewMediator.registerCentralSurfaces(mCentralSurfaces, null, null, null, null); @@ -1518,10 +1384,4 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { private void captureKeyguardUpdateMonitorCallback() { verify(mUpdateMonitor).registerCallback(mKeyguardUpdateMonitorCallbackCaptor.capture()); } - - private void setCurrentUser(int userId, boolean isSecure) { - when(mUserTracker.getUserId()).thenReturn(userId); - when(mSelectedUserInteractor.getSelectedUserId()).thenReturn(userId); - when(mLockPatternUtils.isSecure(userId)).thenReturn(isSecure); - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java index 86094d1a0fef..88c2697fe2f2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java @@ -1519,7 +1519,7 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { assertThat(getNumberOfConnectDeviceButtons()).isEqualTo(1); } - @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING) @Test public void selectedDevicesAddedInSameOrder() { when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(true); @@ -1537,7 +1537,7 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { assertThat(items.get(1).getMediaDevice().get()).isEqualTo(mMediaDevice2); } - @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING) @Test public void selectedDevicesAddedInReverseOrder() { when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(true); @@ -1555,7 +1555,7 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { assertThat(items.get(1).getMediaDevice().get()).isEqualTo(mMediaDevice1); } - @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING) + @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING) @Test public void firstSelectedDeviceIsFirstDeviceInGroupIsTrue() { when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(true); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt index 732561e0979b..944604f94ce4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt @@ -640,11 +640,11 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } } - @DisableFlags(FLAG_GLANCEABLE_HUB_V2) @Test fun onTouchEvent_shadeInteracting_movesNotDispatched() = with(kosmos) { testScope.runTest { + `whenever`(communalViewModel.swipeToHubEnabled()).thenReturn(true) // On lockscreen. goToScene(CommunalScenes.Blank) whenever( @@ -721,11 +721,11 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } } - @DisableFlags(FLAG_GLANCEABLE_HUB_V2) @Test fun onTouchEvent_bouncerInteracting_movesNotDispatched() = with(kosmos) { testScope.runTest { + `whenever`(communalViewModel.swipeToHubEnabled()).thenReturn(true) // On lockscreen. goToScene(CommunalScenes.Blank) whenever( diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierFlagDisabledTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierFlagDisabledTest.java new file mode 100644 index 000000000000..09fa3871f6e3 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierFlagDisabledTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row; + +import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildAcceptableNotificationEntry; + +import static com.google.common.truth.Truth.assertThat; + +import android.compat.testing.PlatformCompatChangeRule; +import android.platform.test.annotations.DisableFlags; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.server.notification.Flags; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; + +import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class NotificationCustomContentMemoryVerifierFlagDisabledTest extends SysuiTestCase { + + @Rule + public PlatformCompatChangeRule mCompatChangeRule = new PlatformCompatChangeRule(); + + @Test + @DisableFlags(Flags.FLAG_NOTIFICATION_CUSTOM_VIEW_URI_RESTRICTION) + @EnableCompatChanges({ + NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS + }) + public void requiresImageViewMemorySizeCheck_flagDisabled_returnsFalse() { + NotificationEntry entry = buildAcceptableNotificationEntry(mContext); + assertThat(NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry)) + .isFalse(); + } + +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java new file mode 100644 index 000000000000..1cadb3c0a909 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java @@ -0,0 +1,285 @@ +/* + * 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.statusbar.notification.row; + +import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildAcceptableNotification; +import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildAcceptableNotificationEntry; +import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildOversizedNotification; +import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildWarningSizedNotification; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.Notification; +import android.compat.testing.PlatformCompatChangeRule; +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.pm.ProviderInfo; +import android.content.res.AssetFileDescriptor; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; +import android.os.Process; +import android.platform.test.annotations.EnableFlags; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.RemoteViews; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.server.notification.Flags; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; + +import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges; +import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.FileNotFoundException; + +@SmallTest +@RunWith(AndroidJUnit4.class) +@EnableFlags(Flags.FLAG_NOTIFICATION_CUSTOM_VIEW_URI_RESTRICTION) +public class NotificationCustomContentMemoryVerifierTest extends SysuiTestCase { + + private static final String AUTHORITY = "notification.memory.test.authority"; + private static final Uri TEST_URI = new Uri.Builder() + .scheme("content") + .authority(AUTHORITY) + .path("path") + .build(); + + @Rule + public PlatformCompatChangeRule mCompatChangeRule = new PlatformCompatChangeRule(); + + @Before + public void setUp() { + TestImageContentProvider provider = new TestImageContentProvider(mContext); + mContext.getContentResolver().addProvider(AUTHORITY, provider); + provider.onCreate(); + } + + @Test + @EnableCompatChanges({ + NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS}) + public void requiresImageViewMemorySizeCheck_customViewNotification_returnsTrue() { + NotificationEntry entry = + buildAcceptableNotificationEntry( + mContext); + assertThat(NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry)) + .isTrue(); + } + + @Test + @EnableCompatChanges({ + NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS}) + public void requiresImageViewMemorySizeCheck_plainNotification_returnsFalse() { + Notification notification = + new Notification.Builder(mContext, "ChannelId") + .setContentTitle("Just a notification") + .setContentText("Yep") + .build(); + NotificationEntry entry = new NotificationEntryBuilder().setNotification( + notification).build(); + assertThat(NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry)) + .isFalse(); + } + + + @Test + @EnableCompatChanges({ + NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS}) + public void satisfiesMemoryLimits_smallNotification_returnsTrue() { + Notification.Builder notification = + buildAcceptableNotification(mContext, + TEST_URI); + NotificationEntry entry = toEntry(notification); + View inflatedView = inflateNotification(notification); + assertThat( + NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(inflatedView, entry) + ) + .isTrue(); + } + + @Test + @EnableCompatChanges({ + NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS}) + public void satisfiesMemoryLimits_oversizedNotification_returnsFalse() { + Notification.Builder notification = + buildOversizedNotification(mContext, + TEST_URI); + NotificationEntry entry = toEntry(notification); + View inflatedView = inflateNotification(notification); + assertThat( + NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(inflatedView, entry) + ).isFalse(); + } + + @Test + @DisableCompatChanges( + {NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS} + ) + public void satisfiesMemoryLimits_oversizedNotification_compatDisabled_returnsTrue() { + Notification.Builder notification = + buildOversizedNotification(mContext, + TEST_URI); + NotificationEntry entry = toEntry(notification); + View inflatedView = inflateNotification(notification); + assertThat( + NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(inflatedView, entry) + ).isTrue(); + } + + @Test + @EnableCompatChanges({ + NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS}) + public void satisfiesMemoryLimits_warningSizedNotification_returnsTrue() { + Notification.Builder notification = + buildWarningSizedNotification(mContext, + TEST_URI); + NotificationEntry entry = toEntry(notification); + View inflatedView = inflateNotification(notification); + assertThat( + NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(inflatedView, entry) + ) + .isTrue(); + } + + @Test + @EnableCompatChanges({ + NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS}) + public void satisfiesMemoryLimits_viewWithoutCustomNotificationRoot_returnsTrue() { + NotificationEntry entry = new NotificationEntryBuilder().build(); + View view = new FrameLayout(mContext); + assertThat(NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(view, entry)) + .isTrue(); + } + + @Test + @EnableCompatChanges({ + NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS}) + public void computeViewHierarchyImageViewSize_smallNotification_returnsSensibleValue() { + Notification.Builder notification = + buildAcceptableNotification(mContext, + TEST_URI); + // This should have a size of a single image + View inflatedView = inflateNotification(notification); + assertThat( + NotificationCustomContentMemoryVerifier.computeViewHierarchyImageViewSize( + inflatedView)) + .isGreaterThan(170000); + } + + private View inflateNotification(Notification.Builder builder) { + RemoteViews remoteViews = builder.createBigContentView(); + return remoteViews.apply(mContext, new FrameLayout(mContext)); + } + + private NotificationEntry toEntry(Notification.Builder builder) { + return new NotificationEntryBuilder().setNotification(builder.build()) + .setUid(Process.myUid()).build(); + } + + + /** This provider serves the images for inflation. */ + class TestImageContentProvider extends ContentProvider { + + TestImageContentProvider(Context context) { + ProviderInfo info = new ProviderInfo(); + info.authority = AUTHORITY; + info.exported = true; + attachInfoForTesting(context, info); + setAuthorities(AUTHORITY); + } + + @Override + public boolean onCreate() { + return true; + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) { + return getContext().getResources().openRawResourceFd( + NotificationCustomContentNotificationBuilder.getDRAWABLE_IMAGE_RESOURCE()) + .getParcelFileDescriptor(); + } + + @Override + public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts) { + return getContext().getResources().openRawResourceFd( + NotificationCustomContentNotificationBuilder.getDRAWABLE_IMAGE_RESOURCE()); + } + + @Override + public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts, + CancellationSignal signal) throws FileNotFoundException { + return openTypedAssetFile(uri, mimeTypeFilter, opts); + } + + @Override + public int delete(Uri uri, Bundle extras) { + return 0; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + return 0; + } + + @Override + public String getType(Uri uri) { + return "image/png"; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + return null; + } + + @Override + public Uri insert(Uri uri, ContentValues values, Bundle extras) { + return super.insert(uri, values, extras); + } + + @Override + public Cursor query(Uri uri, String[] projection, Bundle queryArgs, + CancellationSignal cancellationSignal) { + return super.query(uri, projection, queryArgs, cancellationSignal); + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + return null; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + return 0; + } + } + + +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentNotificationBuilder.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentNotificationBuilder.kt new file mode 100644 index 000000000000..ca4f24da3c08 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentNotificationBuilder.kt @@ -0,0 +1,94 @@ +/* + * 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 + */ + +@file:JvmName("NotificationCustomContentNotificationBuilder") + +package com.android.systemui.statusbar.notification.row + +import android.app.Notification +import android.app.Notification.DecoratedCustomViewStyle +import android.content.Context +import android.graphics.drawable.BitmapDrawable +import android.net.Uri +import android.os.Process +import android.widget.RemoteViews +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.tests.R +import org.hamcrest.Matchers.lessThan +import org.junit.Assume.assumeThat + +public val DRAWABLE_IMAGE_RESOURCE = R.drawable.romainguy_rockaway + +fun buildAcceptableNotificationEntry(context: Context): NotificationEntry { + return NotificationEntryBuilder() + .setNotification(buildAcceptableNotification(context, null).build()) + .setUid(Process.myUid()) + .build() +} + +fun buildAcceptableNotification(context: Context, uri: Uri?): Notification.Builder = + buildNotification(context, uri, 1) + +fun buildOversizedNotification(context: Context, uri: Uri): Notification.Builder { + val numImagesForOversize = + (NotificationCustomContentMemoryVerifier.getStripViewSizeLimit(context) / + drawableSizeOnDevice(context)) + 2 + return buildNotification(context, uri, numImagesForOversize) +} + +fun buildWarningSizedNotification(context: Context, uri: Uri): Notification.Builder { + val numImagesForOversize = + (NotificationCustomContentMemoryVerifier.getWarnViewSizeLimit(context) / + drawableSizeOnDevice(context)) + 1 + // The size needs to be smaller than outright stripping size. + assumeThat( + numImagesForOversize * drawableSizeOnDevice(context), + lessThan(NotificationCustomContentMemoryVerifier.getStripViewSizeLimit(context)), + ) + return buildNotification(context, uri, numImagesForOversize) +} + +fun buildNotification(context: Context, uri: Uri?, numImages: Int): Notification.Builder { + val remoteViews = RemoteViews(context.packageName, R.layout.custom_view_flipper) + repeat(numImages) { i -> + val remoteViewFlipperImageView = + RemoteViews(context.packageName, R.layout.custom_view_flipper_image) + + if (uri == null) { + remoteViewFlipperImageView.setImageViewResource( + R.id.imageview, + R.drawable.romainguy_rockaway, + ) + } else { + val imageUri = uri.buildUpon().appendPath(i.toString()).build() + remoteViewFlipperImageView.setImageViewUri(R.id.imageview, imageUri) + } + remoteViews.addView(R.id.flipper, remoteViewFlipperImageView) + } + + return Notification.Builder(context, "ChannelId") + .setSmallIcon(android.R.drawable.ic_info) + .setStyle(DecoratedCustomViewStyle()) + .setCustomContentView(remoteViews) + .setCustomBigContentView(remoteViews) + .setContentTitle("This is a remote view!") +} + +fun drawableSizeOnDevice(context: Context): Int { + val drawable = context.resources.getDrawable(DRAWABLE_IMAGE_RESOURCE) + return (drawable as BitmapDrawable).bitmap.allocationByteCount +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java index a5234883ed77..14a1233045bb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java @@ -290,7 +290,8 @@ public class ScrimControllerTest extends SysuiTestCase { mKeyguardInteractor, mKosmos.getTestDispatcher(), mLinearLargeScreenShadeInterpolator, - new BlurConfig(0.0f, 0.0f)); + new BlurConfig(0.0f, 0.0f), + mKosmos::getWindowRootViewBlurInteractor); mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible); mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront); mScrimController.setAnimatorListener(mAnimatorListener); @@ -1204,7 +1205,8 @@ public class ScrimControllerTest extends SysuiTestCase { mKeyguardInteractor, mKosmos.getTestDispatcher(), mLinearLargeScreenShadeInterpolator, - new BlurConfig(0.0f, 0.0f)); + new BlurConfig(0.0f, 0.0f), + mKosmos::getWindowRootViewBlurInteractor); mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible); mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront); mScrimController.setAnimatorListener(mAnimatorListener); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java index 2f30b745a4a3..3190d3ae8f16 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java @@ -91,11 +91,11 @@ import com.android.systemui.statusbar.notification.collection.provider.LaunchFul import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.notification.data.repository.NotificationLaunchAnimationRepository; import com.android.systemui.statusbar.notification.domain.interactor.NotificationLaunchAnimationInteractor; +import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationTestHelper; import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; -import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; @@ -122,7 +122,6 @@ import java.util.Optional; @TestableLooper.RunWithLooper(setAsMainLooper = true) public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { - private static final int DISPLAY_ID = 0; private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); @Mock @@ -233,7 +232,6 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { mNotificationActivityStarter = new StatusBarNotificationActivityStarter( getContext(), - DISPLAY_ID, mHandler, mUiBgExecutor, mVisibilityProvider, diff --git a/packages/SystemUI/tests/utils/src/android/os/UserManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/os/UserManagerKosmos.kt index c936b914f44e..26618484669b 100644 --- a/packages/SystemUI/tests/utils/src/android/os/UserManagerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/android/os/UserManagerKosmos.kt @@ -17,6 +17,15 @@ package android.os import com.android.systemui.kosmos.Kosmos -import com.android.systemui.util.mockito.mock +import com.android.systemui.user.data.repository.FakeUserRepository.Companion.MAIN_USER_ID +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever -var Kosmos.userManager by Kosmos.Fixture { mock<UserManager>() } +var Kosmos.userManager by + Kosmos.Fixture { + mock<UserManager> { + whenever(it.mainUser).thenReturn(UserHandle(MAIN_USER_ID)) + whenever(it.getUserSerialNumber(eq(MAIN_USER_ID))).thenReturn(0) + } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt index b0a6de1f931a..0f21a16147f0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt @@ -42,7 +42,9 @@ import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.settings.userTracker import com.android.systemui.statusbar.phone.fakeManagedProfileController +import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.user.domain.interactor.userLockedInteractor import com.android.systemui.util.mockito.mock val Kosmos.communalInteractor by Fixture { @@ -70,6 +72,7 @@ val Kosmos.communalInteractor by Fixture { batteryInteractor = batteryInteractor, dockManager = dockManager, posturingInteractor = posturingInteractor, + userLockedInteractor = userLockedInteractor, ) } @@ -98,10 +101,8 @@ suspend fun Kosmos.setCommunalV2Enabled(enabled: Boolean) { suspend fun Kosmos.setCommunalAvailable(available: Boolean) { setCommunalEnabled(available) - with(fakeKeyguardRepository) { - setIsEncryptedOrLockdown(!available) - setKeyguardShowing(available) - } + fakeKeyguardRepository.setKeyguardShowing(available) + fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, available) } suspend fun Kosmos.setCommunalV2Available(available: Boolean) { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelKosmos.kt index 2e59788663f7..4480539b9a15 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelKosmos.kt @@ -16,18 +16,20 @@ package com.android.systemui.dreams.ui.viewmodel -import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.domain.interactor.shadeModeInteractor -val Kosmos.dreamUserActionsViewModel by - Kosmos.Fixture { - DreamUserActionsViewModel( - communalInteractor = communalInteractor, - deviceUnlockedInteractor = deviceUnlockedInteractor, - shadeInteractor = shadeInteractor, - shadeModeInteractor = shadeModeInteractor, - ) +val Kosmos.dreamUserActionsViewModelFactory by Fixture { + object : DreamUserActionsViewModel.Factory { + override fun create(): DreamUserActionsViewModel { + return DreamUserActionsViewModel( + deviceUnlockedInteractor = deviceUnlockedInteractor, + shadeInteractor = shadeInteractor, + shadeModeInteractor = shadeModeInteractor, + ) + } } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt index fcdda9f13099..9da8e80283b6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.domain.interactor import android.service.dream.dreamManager import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.domain.interactor.communalSceneInteractor +import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.kosmos.Kosmos @@ -44,5 +45,6 @@ var Kosmos.fromDozingTransitionInteractor by deviceEntryInteractor = deviceEntryInteractor, wakeToGoneInteractor = keyguardWakeDirectlyToGoneInteractor, dreamManager = dreamManager, + communalSettingsInteractor = communalSettingsInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelKosmos.kt index ec83157eb108..4f1774f957d6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelKosmos.kt @@ -16,7 +16,6 @@ package com.android.systemui.keyguard.ui.viewmodel -import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture @@ -27,7 +26,6 @@ import com.android.systemui.shade.domain.interactor.shadeModeInteractor val Kosmos.lockscreenUserActionsViewModel by Fixture { LockscreenUserActionsViewModel( deviceEntryInteractor = deviceEntryInteractor, - communalInteractor = communalInteractor, shadeInteractor = shadeInteractor, shadeModeInteractor = shadeModeInteractor, occlusionInteractor = sceneContainerOcclusionInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt index b255b51281af..044332981bf8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt @@ -57,12 +57,10 @@ fun Kosmos.useUnconfinedTestDispatcher() = apply { testDispatcher = UnconfinedTe var Kosmos.testScope by Fixture { TestScope(testDispatcher) } var Kosmos.backgroundScope by Fixture { testScope.backgroundScope } -var Kosmos.applicationCoroutineScope by Fixture { backgroundScope } +var Kosmos.applicationCoroutineScope by Fixture { testScope.backgroundScope } var Kosmos.testCase: SysuiTestCase by Fixture() -var Kosmos.backgroundCoroutineContext: CoroutineContext by Fixture { - backgroundScope.coroutineContext -} -var Kosmos.mainCoroutineContext: CoroutineContext by Fixture { testScope.coroutineContext } +var Kosmos.backgroundCoroutineContext: CoroutineContext by Fixture { testDispatcher } +var Kosmos.mainCoroutineContext: CoroutineContext by Fixture { testDispatcher } /** * Run this test body with a [Kosmos] as receiver, and using the [testScope] currently installed in diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt index 446e10671afb..60b371aa8afb 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt @@ -90,6 +90,7 @@ import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisionin import com.android.systemui.statusbar.ui.viewmodel.keyguardStatusBarViewModel import com.android.systemui.util.time.systemClock import com.android.systemui.volume.domain.interactor.volumeDialogInteractor +import com.android.systemui.window.domain.interactor.windowRootViewBlurInteractor /** * Helper for using [Kosmos] from Java. @@ -192,4 +193,5 @@ class KosmosJavaAdapter() { val disableFlagsInteractor by lazy { kosmos.disableFlagsInteractor } val fakeDisableFlagsRepository by lazy { kosmos.fakeDisableFlagsRepository } val mockWindowRootViewProvider by lazy { kosmos.mockWindowRootViewProvider } + val windowRootViewBlurInteractor by lazy { kosmos.windowRootViewBlurInteractor } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt index 5fc31f8b9e10..f2871149de11 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt @@ -19,6 +19,7 @@ package com.android.systemui.qs.pipeline.data.repository import com.android.systemui.qs.pipeline.data.model.RestoreData import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository.Companion.POSITION_AT_END import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.TilesUpgradePath import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -79,9 +80,9 @@ class FakeTileSpecRepository( with(getFlow(userId)) { value = defaultTilesRepository.defaultTiles } } - override val tilesReadFromSetting: Channel<Pair<Set<TileSpec>, Int>> = Channel(capacity = 10) + override val tilesUpgradePath: Channel<Pair<TilesUpgradePath, Int>> = Channel(capacity = 10) - suspend fun sendTilesReadFromSetting(tiles: Set<TileSpec>, userId: Int) { - tilesReadFromSetting.send(tiles to userId) + suspend fun sendTilesFromUpgradePath(upgradePath: TilesUpgradePath, userId: Int) { + tilesUpgradePath.send(upgradePath to userId) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt index 5ff44e5d33c5..c5de02a7281b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt @@ -26,7 +26,7 @@ val Kosmos.minimumTilesRepository: MinimumTilesRepository by Kosmos.Fixture { fakeMinimumTilesRepository } var Kosmos.fakeDefaultTilesRepository by Kosmos.Fixture { FakeDefaultTilesRepository() } -val Kosmos.defaultTilesRepository: DefaultTilesRepository by +var Kosmos.defaultTilesRepository: DefaultTilesRepository by Kosmos.Fixture { fakeDefaultTilesRepository } val Kosmos.fakeTileSpecRepository by 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 ce298bb90ba2..f0350acd83ca 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 @@ -5,6 +5,8 @@ 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.aodBurnInViewModel +import com.android.systemui.keyguard.ui.viewmodel.keyguardClockViewModel import com.android.systemui.keyguard.ui.viewmodel.lightRevealScrimViewModel import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture @@ -18,7 +20,6 @@ import com.android.systemui.scene.ui.FakeOverlay import com.android.systemui.scene.ui.composable.ConstantSceneContainerTransitionsBuilder import com.android.systemui.scene.ui.viewmodel.SceneContainerHapticsViewModel import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel -import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.domain.interactor.shadeModeInteractor import com.android.systemui.statusbar.domain.interactor.remoteInputInteractor @@ -97,7 +98,6 @@ val Kosmos.sceneContainerViewModelFactory by Fixture { powerInteractor = powerInteractor, shadeModeInteractor = shadeModeInteractor, remoteInputInteractor = remoteInputInteractor, - splitEdgeDetector = splitEdgeDetector, logger = sceneLogger, hapticsViewModelFactory = sceneContainerHapticsViewModelFactory, view = view, @@ -105,6 +105,8 @@ val Kosmos.sceneContainerViewModelFactory by Fixture { lightRevealScrim = lightRevealScrimViewModel, wallpaperViewModel = wallpaperViewModel, keyguardInteractor = keyguardInteractor, + burnIn = aodBurnInViewModel, + clock = keyguardClockViewModel, ) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt index eb352baab0e4..2efc09f2682f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt @@ -23,6 +23,7 @@ import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.scene.data.repository.sceneContainerRepository import com.android.systemui.scene.domain.resolver.sceneFamilyResolvers import com.android.systemui.scene.shared.logger.sceneLogger +import com.android.systemui.shade.domain.interactor.shadeModeInteractor val Kosmos.sceneInteractor: SceneInteractor by Kosmos.Fixture { @@ -34,5 +35,6 @@ val Kosmos.sceneInteractor: SceneInteractor by deviceUnlockedInteractor = { deviceUnlockedInteractor }, keyguardEnabledInteractor = { keyguardEnabledInteractor }, disabledContentInteractor = disabledContentInteractor, + shadeModeInteractor = shadeModeInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt index f5eebb46c2ec..60c0f342b874 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt @@ -77,6 +77,14 @@ class FakeSceneDataSource(initialSceneKey: SceneKey, val testScope: TestScope) : showOverlay(to, transitionKey) } + override fun instantlyShowOverlay(overlay: OverlayKey) { + showOverlay(overlay) + } + + override fun instantlyHideOverlay(overlay: OverlayKey) { + hideOverlay(overlay) + } + /** * Pauses scene and overlay changes. * diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt index b40e1e7ab33b..6b641934bc44 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.data.repository import com.android.systemui.statusbar.notification.data.model.activeNotificationModel +import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel /** * Make the repository hold [count] active notifications for testing. The keys of the notifications @@ -37,3 +38,56 @@ fun ActiveNotificationListRepository.setActiveNotifs(count: Int) { } .build() } + +/** + * Adds the given notification to the repository while *maintaining any notifications already + * present*. [notif] will be ranked highest. + */ +fun ActiveNotificationListRepository.addNotif(notif: ActiveNotificationModel) { + val currentNotifications = this.activeNotifications.value.individuals + this.activeNotifications.value = + ActiveNotificationsStore.Builder() + .apply { + addIndividualNotif(notif) + currentNotifications.forEach { + if (it.key != notif.key) { + addIndividualNotif(it.value) + } + } + } + .build() +} + +/** + * Adds the given notification to the repository while *maintaining any notifications already + * present*. [notifs] will be ranked higher than existing notifs. + */ +fun ActiveNotificationListRepository.addNotifs(notifs: List<ActiveNotificationModel>) { + val currentNotifications = this.activeNotifications.value.individuals + val newKeys = notifs.map { it.key } + this.activeNotifications.value = + ActiveNotificationsStore.Builder() + .apply { + notifs.forEach { addIndividualNotif(it) } + currentNotifications.forEach { + if (!newKeys.contains(it.key)) { + addIndividualNotif(it.value) + } + } + } + .build() +} + +fun ActiveNotificationListRepository.removeNotif(keyToRemove: String) { + val currentNotifications = this.activeNotifications.value.individuals + this.activeNotifications.value = + ActiveNotificationsStore.Builder() + .apply { + currentNotifications.forEach { + if (it.key != keyToRemove) { + addIndividualNotif(it.value) + } + } + } + .build() +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorKosmos.kt index 2057b849c069..c7380c91f703 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorKosmos.kt @@ -21,6 +21,7 @@ import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.shade.domain.interactor.shadeModeInteractor import com.android.systemui.statusbar.lockscreenShadeTransitionController val Kosmos.notificationShelfInteractor by Fixture { @@ -28,6 +29,7 @@ val Kosmos.notificationShelfInteractor by Fixture { keyguardRepository = keyguardRepository, deviceEntryFaceAuthRepository = deviceEntryFaceAuthRepository, powerInteractor = powerInteractor, + shadeModeInteractor = shadeModeInteractor, keyguardTransitionController = lockscreenShadeTransitionController, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt index 0d6ac4481742..d787e2c190c8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt @@ -52,7 +52,6 @@ val Kosmos.statusBarNotificationActivityStarter by Kosmos.Fixture { StatusBarNotificationActivityStarter( applicationContext, - applicationContext.displayId, fakeExecutorHandler, fakeExecutor, notificationVisibilityProvider, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModelBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModelBuilder.kt deleted file mode 100644 index 923b36d4f2cf..000000000000 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModelBuilder.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.phone.ongoingcall.shared.model - -import android.app.PendingIntent -import com.android.systemui.statusbar.StatusBarIconView -import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel - -/** Helper for building [OngoingCallModel.InCall] instances in tests. */ -fun inCallModel( - startTimeMs: Long, - notificationIcon: StatusBarIconView? = null, - intent: PendingIntent? = null, - notificationKey: String = "test", - appName: String = "", - promotedContent: PromotedNotificationContentModel? = null, -) = - OngoingCallModel.InCall( - startTimeMs, - notificationIcon, - intent, - notificationKey, - appName, - promotedContent, - ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt new file mode 100644 index 000000000000..d09d010cba2e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.phone.ongoingcall.shared.model + +import android.app.PendingIntent +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.StatusBarIconView +import com.android.systemui.statusbar.core.StatusBarConnectedDisplays +import com.android.systemui.statusbar.notification.data.model.activeNotificationModel +import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository +import com.android.systemui.statusbar.notification.data.repository.addNotif +import com.android.systemui.statusbar.notification.data.repository.removeNotif +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.notification.shared.CallType +import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization +import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository +import org.mockito.kotlin.mock + +/** Helper for building [OngoingCallModel.InCall] instances in tests. */ +fun inCallModel( + startTimeMs: Long, + notificationIcon: StatusBarIconView? = null, + intent: PendingIntent? = null, + notificationKey: String = "test", + appName: String = "", + promotedContent: PromotedNotificationContentModel? = null, +) = + OngoingCallModel.InCall( + startTimeMs, + notificationIcon, + intent, + notificationKey, + appName, + promotedContent, + ) + +object OngoingCallTestHelper { + /** + * Removes any ongoing call state and removes any call notification associated with [key]. Does + * it correctly based on whether [StatusBarChipsModernization] is enabled or not. + * + * @param key the notification key associated with the call notification. + */ + fun Kosmos.removeOngoingCallState(key: String) { + if (StatusBarChipsModernization.isEnabled) { + activeNotificationListRepository.removeNotif(key) + } else { + ongoingCallRepository.setOngoingCallState(OngoingCallModel.NoCall) + } + } + + /** + * Sets SysUI to have an ongoing call state. Does it correctly based on whether + * [StatusBarChipsModernization] is enabled or not. + * + * @param key the notification key to be associated with the call notification + */ + fun Kosmos.addOngoingCallState( + key: String = "notif", + startTimeMs: Long = 1000L, + statusBarChipIconView: StatusBarIconView? = createStatusBarIconViewOrNull(), + promotedContent: PromotedNotificationContentModel? = null, + contentIntent: PendingIntent? = null, + uid: Int = DEFAULT_UID, + ) { + if (StatusBarChipsModernization.isEnabled) { + activeNotificationListRepository.addNotif( + activeNotificationModel( + key = key, + whenTime = startTimeMs, + callType = CallType.Ongoing, + statusBarChipIcon = statusBarChipIconView, + contentIntent = contentIntent, + promotedContent = promotedContent, + uid = uid, + ) + ) + } else { + ongoingCallRepository.setOngoingCallState( + inCallModel( + startTimeMs = startTimeMs, + notificationIcon = statusBarChipIconView, + intent = contentIntent, + notificationKey = key, + promotedContent = promotedContent, + ) + ) + } + } + + private fun createStatusBarIconViewOrNull(): StatusBarIconView? = + if (StatusBarConnectedDisplays.isEnabled) { + null + } else { + mock<StatusBarIconView>() + } + + private const val DEFAULT_UID = 886 +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateKosmos.kt index ef043e0177a5..2c0bd326525c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateKosmos.kt @@ -19,6 +19,8 @@ package com.android.systemui.statusbar.policy.ui.dialog import android.content.mockedContext import com.android.systemui.animation.dialogTransitionAnimator import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.backgroundCoroutineContext import com.android.systemui.kosmos.mainCoroutineContext import com.android.systemui.plugins.activityStarter import com.android.systemui.shade.data.repository.shadeDialogContextInteractor @@ -37,7 +39,9 @@ var Kosmos.modesDialogDelegate: ModesDialogDelegate by activityStarter, { modesDialogViewModel }, modesDialogEventLogger, + applicationCoroutineScope, mainCoroutineContext, + backgroundCoroutineContext, shadeDialogContextInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelKosmos.kt index 3571a737704b..4710813087d3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelKosmos.kt @@ -16,7 +16,7 @@ package com.android.systemui.statusbar.policy.ui.dialog.viewmodel -import android.content.mockedContext +import android.content.applicationContext import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testDispatcher import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor @@ -27,7 +27,7 @@ import javax.inject.Provider val Kosmos.modesDialogViewModel: ModesDialogViewModel by Kosmos.Fixture { ModesDialogViewModel( - mockedContext, + applicationContext, zenModeInteractor, testDispatcher, Provider { modesDialogDelegate }.get(), diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt index 85d582a27faf..145bb93d9cb0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt @@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update import kotlinx.coroutines.yield @SysUISingleton @@ -76,6 +77,11 @@ class FakeUserRepository @Inject constructor() : UserRepository { override val isLogoutToSystemUserEnabled: StateFlow<Boolean> = _isLogoutToSystemUserEnabled.asStateFlow() + private val _userUnlockedState = MutableStateFlow(emptyMap<UserHandle, Boolean>()) + + override fun isUserUnlocked(userHandle: UserHandle?): Flow<Boolean> = + _userUnlockedState.map { it[userHandle] ?: false } + override var mainUserId: Int = MAIN_USER_ID override var lastSelectedNonGuestUserId: Int = mainUserId @@ -176,6 +182,14 @@ class FakeUserRepository @Inject constructor() : UserRepository { fun setGuestUserAutoCreated(value: Boolean) { _isGuestUserAutoCreated = value } + + fun setUserUnlocked(userId: Int, unlocked: Boolean) { + setUserUnlocked(UserHandle(userId), unlocked) + } + + fun setUserUnlocked(userHandle: UserHandle, unlocked: Boolean) { + _userUnlockedState.update { it + (userHandle to unlocked) } + } } @Module diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt index e0b529261c4d..fd955089cdc7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt @@ -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,16 +14,10 @@ * limitations under the License. */ -package com.android.systemui.scene.ui.viewmodel +package com.android.systemui.user.domain.interactor -import androidx.compose.ui.unit.dp import com.android.systemui.kosmos.Kosmos -import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.user.data.repository.userRepository -var Kosmos.splitEdgeDetector: SplitEdgeDetector by - Kosmos.Fixture { - SplitEdgeDetector( - topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction, - edgeSize = 40.dp, - ) - } +val Kosmos.userLockedInteractor by + Kosmos.Fixture { UserLockedInteractor(userRepository = userRepository) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt index 0d2aa4c79753..888b7e625524 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt @@ -19,6 +19,7 @@ package com.android.systemui.volume.dialog.domain.interactor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.plugins.volumeDialogController +import com.android.systemui.shared.settings.data.repository.secureSettingsRepository import com.android.systemui.volume.dialog.data.repository.volumeDialogVisibilityRepository import com.android.systemui.volume.dialog.utils.volumeTracer @@ -30,5 +31,6 @@ val Kosmos.volumeDialogVisibilityInteractor by volumeTracer, volumeDialogVisibilityRepository, volumeDialogController, + secureSettingsRepository, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt index 7281e03a5ea4..96992233375d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt @@ -17,5 +17,16 @@ package com.android.systemui.window.data.repository import com.android.systemui.kosmos.Kosmos +import kotlinx.coroutines.flow.MutableStateFlow -val Kosmos.windowRootViewBlurRepository by Kosmos.Fixture { WindowRootViewBlurRepository() } +val Kosmos.fakeWindowRootViewBlurRepository: FakeWindowRootViewBlurRepository by + Kosmos.Fixture { FakeWindowRootViewBlurRepository() } + +val Kosmos.windowRootViewBlurRepository: WindowRootViewBlurRepository by + Kosmos.Fixture { fakeWindowRootViewBlurRepository } + +class FakeWindowRootViewBlurRepository : WindowRootViewBlurRepository { + override val blurRadius: MutableStateFlow<Int> = MutableStateFlow(0) + override val isBlurOpaque: MutableStateFlow<Boolean> = MutableStateFlow(false) + override val isBlurSupported: MutableStateFlow<Boolean> = MutableStateFlow(false) +} diff --git a/services/accessibility/OWNERS b/services/accessibility/OWNERS index 4e1175034b5b..ab1e9ffe3bfe 100644 --- a/services/accessibility/OWNERS +++ b/services/accessibility/OWNERS @@ -1,4 +1,7 @@ -# Bug component: 44215 +# Bug component: 1530954 +# +# The above component is for automated test bugs. If you are a human looking to report +# a bug in this codebase then please use component 44215. # Android Accessibility Framework owners danielnorman@google.com diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig index e8dddcb537cd..529a564ea607 100644 --- a/services/accessibility/accessibility.aconfig +++ b/services/accessibility/accessibility.aconfig @@ -100,6 +100,13 @@ flag { } flag { + name: "enable_low_vision_generic_feedback" + namespace: "accessibility" + description: "Use generic feedback for low vision." + bug: "393981463" +} + +flag { name: "enable_low_vision_hats" namespace: "accessibility" description: "Use HaTS for low vision feedback." diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java index 8e448676c214..db8441d2424b 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java @@ -510,6 +510,11 @@ public class AutoclickController extends BaseEventStreamTransformation { return mMetaState; } + @VisibleForTesting + boolean getIsActiveForTesting() { + return mActive; + } + /** * Updates delay that should be used when scheduling clicks. The delay will be used only for * clicks scheduled after this point (pending click tasks are not affected). diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java index 0354d2be60c9..2ef11f4b78e1 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java @@ -20,11 +20,14 @@ import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_M import android.content.Context; import android.graphics.PixelFormat; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.WindowInsets; import android.view.WindowManager; +import android.widget.ImageButton; import android.widget.LinearLayout; import androidx.annotation.NonNull; @@ -51,6 +54,8 @@ public class AutoclickTypePanel { private final LinearLayout mDragButton; private final LinearLayout mScrollButton; + private LinearLayout mSelectedButton; + public AutoclickTypePanel(Context context, WindowManager windowManager) { mContext = context; mWindowManager = windowManager; @@ -80,6 +85,40 @@ public class AutoclickTypePanel { // Initializes panel as collapsed state and only displays the left click button. hideAllClickTypeButtons(); mLeftClickButton.setVisibility(View.VISIBLE); + setSelectedButton(/* selectedButton= */ mLeftClickButton); + } + + /** Sets the selected button and updates the newly and previously selected button styling. */ + private void setSelectedButton(@NonNull LinearLayout selectedButton) { + // Updates the previously selected button styling. + if (mSelectedButton != null) { + toggleSelectedButtonStyle(mSelectedButton, /* isSelected= */ false); + } + + mSelectedButton = selectedButton; + + // Updates the newly selected button styling. + toggleSelectedButtonStyle(selectedButton, /* isSelected= */ true); + } + + private void toggleSelectedButtonStyle(@NonNull LinearLayout button, boolean isSelected) { + // Sets icon background color. + GradientDrawable gradientDrawable = (GradientDrawable) button.getBackground(); + gradientDrawable.setColor( + mContext.getColor( + isSelected + ? R.color.materialColorPrimary + : R.color.materialColorSurfaceContainer)); + + // Sets icon color. + ImageButton imageButton = (ImageButton) button.getChildAt(/* index= */ 0); + Drawable drawable = imageButton.getDrawable(); + drawable.mutate() + .setTint( + mContext.getColor( + isSelected + ? R.color.materialColorSurfaceContainer + : R.color.materialColorPrimary)); } public void show() { @@ -97,6 +136,9 @@ public class AutoclickTypePanel { // buttons except the one user selected. hideAllClickTypeButtons(); button.setVisibility(View.VISIBLE); + + // Sets the newly selected button. + setSelectedButton(/* selectedButton= */ button); } else { // If the panel is already collapsed, we just need to expand it. showAllClickTypeButtons(); diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/OWNERS b/services/accessibility/java/com/android/server/accessibility/magnification/OWNERS new file mode 100644 index 000000000000..ff812ad7e7e6 --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/magnification/OWNERS @@ -0,0 +1,8 @@ +# Bug component: 1530954 +# +# The above component is for automated test bugs. If you are a human looking to report +# a bug in this codebase then please use component 770744. + +juchengchou@google.com +chenjean@google.com +chihtinglo@google.com diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java index 43764442e2cf..d0ee7af1bbfb 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java +++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java @@ -27,6 +27,7 @@ import android.annotation.WorkerThread; import android.app.appfunctions.AppFunctionException; import android.app.appfunctions.AppFunctionManager; import android.app.appfunctions.AppFunctionManagerHelper; +import android.app.appfunctions.AppFunctionManagerHelper.AppFunctionNotFoundException; import android.app.appfunctions.AppFunctionRuntimeMetadata; import android.app.appfunctions.AppFunctionStaticMetadataHelper; import android.app.appfunctions.ExecuteAppFunctionAidlRequest; @@ -513,7 +514,9 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { e = e.getCause(); } int resultCode = AppFunctionException.ERROR_SYSTEM_ERROR; - if (e instanceof AppSearchException appSearchException) { + if (e instanceof AppFunctionNotFoundException) { + resultCode = AppFunctionException.ERROR_FUNCTION_NOT_FOUND; + } else if (e instanceof AppSearchException appSearchException) { resultCode = mapAppSearchResultFailureCodeToExecuteAppFunctionResponse( appSearchException.getResultCode()); diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 414db37508e5..05301fdd8385 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -588,10 +588,10 @@ public class CompanionDeviceManagerService extends SystemService { @Override @EnforcePermission(DELIVER_COMPANION_MESSAGES) public void attachSystemDataTransport(String packageName, int userId, int associationId, - ParcelFileDescriptor fd) { + ParcelFileDescriptor fd, int flags) { attachSystemDataTransport_enforcePermission(); - mTransportManager.attachSystemDataTransport(associationId, fd); + mTransportManager.attachSystemDataTransport(associationId, fd, flags); } @Override diff --git a/services/companion/java/com/android/server/companion/securechannel/AttestationVerifier.java b/services/companion/java/com/android/server/companion/securechannel/AttestationVerifier.java index df3071e08a03..42af0597b35c 100644 --- a/services/companion/java/com/android/server/companion/securechannel/AttestationVerifier.java +++ b/services/companion/java/com/android/server/companion/securechannel/AttestationVerifier.java @@ -16,7 +16,9 @@ package com.android.server.companion.securechannel; +import static android.companion.CompanionDeviceManager.TRANSPORT_FLAG_EXTEND_PATCH_DIFF; import static android.security.attestationverification.AttestationVerificationManager.PARAM_CHALLENGE; +import static android.security.attestationverification.AttestationVerificationManager.PARAM_MAX_PATCH_LEVEL_DIFF_MONTHS; import static android.security.attestationverification.AttestationVerificationManager.PROFILE_PEER_DEVICE; import static android.security.attestationverification.AttestationVerificationManager.TYPE_CHALLENGE; @@ -34,15 +36,21 @@ import java.util.function.BiConsumer; /** * Helper class to perform attestation verification synchronously. + * + * @hide */ public class AttestationVerifier { private static final long ATTESTATION_VERIFICATION_TIMEOUT_SECONDS = 10; // 10 seconds private static final String PARAM_OWNED_BY_SYSTEM = "android.key_owned_by_system"; + private static final int EXTENDED_PATCH_LEVEL_DIFF_MONTHS = 24; // 2 years + private final Context mContext; + private final int mFlags; - AttestationVerifier(Context context) { + AttestationVerifier(Context context, int flags) { this.mContext = context; + this.mFlags = flags; } /** @@ -59,10 +67,13 @@ public class AttestationVerifier { @NonNull byte[] remoteAttestation, @NonNull byte[] attestationChallenge ) throws SecureChannelException { - Bundle requirements = new Bundle(); + final Bundle requirements = new Bundle(); requirements.putByteArray(PARAM_CHALLENGE, attestationChallenge); requirements.putBoolean(PARAM_OWNED_BY_SYSTEM, true); // Custom parameter for CDM + // Apply flags to verifier requirements + updateRequirements(requirements); + // Synchronously execute attestation verification. AtomicInteger verificationResult = new AtomicInteger(0); CountDownLatch verificationFinished = new CountDownLatch(1); @@ -96,4 +107,15 @@ public class AttestationVerifier { return verificationResult.get(); } + + private void updateRequirements(Bundle requirements) { + if (mFlags == 0) { + return; + } + + if ((mFlags & TRANSPORT_FLAG_EXTEND_PATCH_DIFF) > 0) { + requirements.putInt(PARAM_MAX_PATCH_LEVEL_DIFF_MONTHS, + EXTENDED_PATCH_LEVEL_DIFF_MONTHS); + } + } } diff --git a/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java b/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java index 2d3782fb3181..6c7c9b3e073d 100644 --- a/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java +++ b/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java @@ -59,6 +59,7 @@ public class SecureChannel { private final Callback mCallback; private final byte[] mPreSharedKey; private final AttestationVerifier mVerifier; + private final int mFlags; private volatile boolean mStopped; private volatile boolean mInProgress; @@ -89,7 +90,7 @@ public class SecureChannel { @NonNull Callback callback, @NonNull byte[] preSharedKey ) { - this(in, out, callback, preSharedKey, null); + this(in, out, callback, preSharedKey, null, 0); } /** @@ -100,14 +101,16 @@ public class SecureChannel { * @param out output stream from which data is sent out * @param callback subscription to received messages from the channel * @param context context for fetching the Attestation Verifier Framework system service + * @param flags flags for custom security settings on the channel */ public SecureChannel( @NonNull final InputStream in, @NonNull final OutputStream out, @NonNull Callback callback, - @NonNull Context context + @NonNull Context context, + int flags ) { - this(in, out, callback, null, new AttestationVerifier(context)); + this(in, out, callback, null, new AttestationVerifier(context, flags), flags); } public SecureChannel( @@ -115,13 +118,15 @@ public class SecureChannel { final OutputStream out, Callback callback, byte[] preSharedKey, - AttestationVerifier verifier + AttestationVerifier verifier, + int flags ) { this.mInput = in; this.mOutput = out; this.mCallback = callback; this.mPreSharedKey = preSharedKey; this.mVerifier = verifier; + this.mFlags = flags; } /** diff --git a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java index 36083607bfcd..92d9fb02de79 100644 --- a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java +++ b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java @@ -16,7 +16,11 @@ package com.android.server.companion.transport; +import static android.companion.AssociationRequest.DEVICE_PROFILE_WEARABLE_SENSING; import static android.companion.CompanionDeviceManager.MESSAGE_REQUEST_PERMISSION_RESTORE; +import static android.companion.CompanionDeviceManager.TRANSPORT_FLAG_EXTEND_PATCH_DIFF; + +import static com.android.server.companion.transport.TransportUtils.enforceAssociationCanUseTransportFlags; import android.annotation.NonNull; import android.annotation.SuppressLint; @@ -152,10 +156,14 @@ public class CompanionTransportManager { /** * Attach transport. */ - public void attachSystemDataTransport(int associationId, ParcelFileDescriptor fd) { + public void attachSystemDataTransport(int associationId, ParcelFileDescriptor fd, + int flags) { Slog.i(TAG, "Attaching transport for association id=[" + associationId + "]..."); - mAssociationStore.getAssociationWithCallerChecks(associationId); + AssociationInfo association = + mAssociationStore.getAssociationWithCallerChecks(associationId); + + enforceAssociationCanUseTransportFlags(association, flags); synchronized (mTransports) { if (mTransports.contains(associationId)) { @@ -163,7 +171,7 @@ public class CompanionTransportManager { } // TODO: Implement new API to pass a PSK - initializeTransport(associationId, fd, null); + initializeTransport(association, fd, null, flags); notifyOnTransportsChanged(); } @@ -217,10 +225,12 @@ public class CompanionTransportManager { } } - private void initializeTransport(int associationId, + private void initializeTransport(AssociationInfo association, ParcelFileDescriptor fd, - byte[] preSharedKey) { + byte[] preSharedKey, + int flags) { Slog.i(TAG, "Initializing transport"); + int associationId = association.getId(); Transport transport; if (!isSecureTransportEnabled()) { // If secure transport is explicitly disabled for testing, use raw transport @@ -230,15 +240,21 @@ public class CompanionTransportManager { // If device is debug build, use hardcoded test key for authentication Slog.d(TAG, "Creating an unauthenticated secure channel"); final byte[] testKey = "CDM".getBytes(StandardCharsets.UTF_8); - transport = new SecureTransport(associationId, fd, mContext, testKey, null); + transport = new SecureTransport(associationId, fd, mContext, testKey, null, 0); } else if (preSharedKey != null) { // If either device is not Android, then use app-specific pre-shared key Slog.d(TAG, "Creating a PSK-authenticated secure channel"); - transport = new SecureTransport(associationId, fd, mContext, preSharedKey, null); + transport = new SecureTransport(associationId, fd, mContext, preSharedKey, null, 0); + } else if (DEVICE_PROFILE_WEARABLE_SENSING.equals(association.getDeviceProfile())) { + // If device is glasses with WEARABLE_SENSING profile, extend the allowed patch + // difference to 2 years instead of 1. + Slog.d(TAG, "Creating a secure channel with extended patch difference allowance"); + transport = new SecureTransport(associationId, fd, mContext, + TRANSPORT_FLAG_EXTEND_PATCH_DIFF); } else { // If none of the above applies, then use secure channel with attestation verification Slog.d(TAG, "Creating a secure channel"); - transport = new SecureTransport(associationId, fd, mContext); + transport = new SecureTransport(associationId, fd, mContext, flags); } addMessageListenersToTransport(transport); diff --git a/services/companion/java/com/android/server/companion/transport/SecureTransport.java b/services/companion/java/com/android/server/companion/transport/SecureTransport.java index 1e95e65848a5..77dc80998e2e 100644 --- a/services/companion/java/com/android/server/companion/transport/SecureTransport.java +++ b/services/companion/java/com/android/server/companion/transport/SecureTransport.java @@ -36,15 +36,22 @@ class SecureTransport extends Transport implements SecureChannel.Callback { private final BlockingQueue<byte[]> mRequestQueue = new ArrayBlockingQueue<>(500); - SecureTransport(int associationId, ParcelFileDescriptor fd, Context context) { + SecureTransport(int associationId, ParcelFileDescriptor fd, Context context, int flags) { super(associationId, fd, context); - mSecureChannel = new SecureChannel(mRemoteIn, mRemoteOut, this, context); + mSecureChannel = new SecureChannel(mRemoteIn, mRemoteOut, this, context, flags); } SecureTransport(int associationId, ParcelFileDescriptor fd, Context context, - byte[] preSharedKey, AttestationVerifier verifier) { + byte[] preSharedKey, AttestationVerifier verifier, int flags) { super(associationId, fd, context); - mSecureChannel = new SecureChannel(mRemoteIn, mRemoteOut, this, preSharedKey, verifier); + mSecureChannel = new SecureChannel( + mRemoteIn, + mRemoteOut, + this, + preSharedKey, + verifier, + flags + ); } @Override diff --git a/services/companion/java/com/android/server/companion/transport/TransportUtils.java b/services/companion/java/com/android/server/companion/transport/TransportUtils.java new file mode 100644 index 000000000000..7a15c11afd19 --- /dev/null +++ b/services/companion/java/com/android/server/companion/transport/TransportUtils.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.companion.transport; + +import static android.companion.AssociationRequest.DEVICE_PROFILE_WEARABLE_SENSING; +import static android.companion.CompanionDeviceManager.TRANSPORT_FLAG_EXTEND_PATCH_DIFF; + +import static java.util.Collections.unmodifiableMap; + +import android.companion.AssociationInfo; +import android.util.ArrayMap; + +import java.util.Map; + +/** + * Utility class for transport manager. + * @hide + */ +public final class TransportUtils { + + /** + * Device profile -> Union of allowlisted transport flags + */ + private static final Map<String, Integer> DEVICE_PROFILE_TRANSPORT_FLAGS_ALLOWLIST; + static { + final Map<String, Integer> map = new ArrayMap<>(); + map.put(DEVICE_PROFILE_WEARABLE_SENSING, + TRANSPORT_FLAG_EXTEND_PATCH_DIFF); + DEVICE_PROFILE_TRANSPORT_FLAGS_ALLOWLIST = unmodifiableMap(map); + } + + /** + * Enforce that the association that is trying to attach a transport with provided flags has + * one of the allowlisted device profiles that may apply the flagged features. + * + * @param association Association for which transport is being attached + * @param flags Flags for features being applied to the transport + */ + public static void enforceAssociationCanUseTransportFlags( + AssociationInfo association, int flags) { + if (flags == 0) { + return; + } + + final String deviceProfile = association.getDeviceProfile(); + if (!DEVICE_PROFILE_TRANSPORT_FLAGS_ALLOWLIST.containsKey(deviceProfile)) { + throw new IllegalArgumentException("Association (id=" + association.getId() + + ") with device profile " + deviceProfile + " does not support the " + + "usage of transport flags."); + } + + int allowedFlags = DEVICE_PROFILE_TRANSPORT_FLAGS_ALLOWLIST.get(deviceProfile); + + // Ensure that every non-zero bits in flags are also present in allowed flags + if ((allowedFlags & flags) != flags) { + throw new IllegalArgumentException("Association (id=" + association.getId() + + ") does not have the device profile required to use at least " + + "one of the flags in this transport."); + } + } + + private TransportUtils() {} +} diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index c385fbad02a5..f03e8c713228 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -30,7 +30,6 @@ import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_BLOCKED_ import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS; -import static android.companion.virtualdevice.flags.Flags.virtualCameraServiceDiscovery; import android.annotation.NonNull; import android.annotation.Nullable; @@ -55,9 +54,9 @@ import android.companion.virtual.VirtualDeviceParams; import android.companion.virtual.audio.IAudioConfigChangedCallback; import android.companion.virtual.audio.IAudioRoutingCallback; import android.companion.virtual.camera.VirtualCameraConfig; -import android.companion.virtual.flags.Flags; import android.companion.virtual.sensor.VirtualSensor; import android.companion.virtual.sensor.VirtualSensorEvent; +import android.companion.virtualdevice.flags.Flags; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledAfter; import android.content.AttributionSource; @@ -111,6 +110,7 @@ import android.util.SparseIntArray; import android.view.Display; import android.view.WindowManager; import android.widget.Toast; +import android.window.DisplayWindowPolicyController; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; @@ -265,7 +265,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub UserHandle.SYSTEM); } - if (android.companion.virtualdevice.flags.Flags.activityControlApi()) { + if (Flags.activityControlApi()) { try { mActivityListener.onActivityLaunchBlocked( displayId, @@ -280,7 +280,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub @Override public void onSecureWindowShown(int displayId, @NonNull ActivityInfo activityInfo) { - if (android.companion.virtualdevice.flags.Flags.activityControlApi()) { + if (Flags.activityControlApi()) { try { mActivityListener.onSecureWindowShown( displayId, @@ -318,7 +318,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub @Override public void onSecureWindowHidden(int displayId) { - if (android.companion.virtualdevice.flags.Flags.activityControlApi()) { + if (Flags.activityControlApi()) { try { mActivityListener.onSecureWindowHidden(displayId); } catch (RemoteException e) { @@ -682,7 +682,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub checkCallerIsDeviceOwner(); final int displayId = exemption.getDisplayId(); if (exemption.getComponentName() == null || displayId != Display.INVALID_DISPLAY) { - if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) { + if (!Flags.activityControlApi()) { return; } } @@ -719,7 +719,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub checkCallerIsDeviceOwner(); final int displayId = exemption.getDisplayId(); if (exemption.getComponentName() == null || displayId != Display.INVALID_DISPLAY) { - if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) { + if (!Flags.activityControlApi()) { return; } } @@ -921,7 +921,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } break; case POLICY_TYPE_BLOCKED_ACTIVITY: - if (android.companion.virtualdevice.flags.Flags.activityControlApi()) { + if (Flags.activityControlApi()) { synchronized (mVirtualDeviceLock) { mDevicePolicies.put(policyType, devicePolicy); } @@ -938,7 +938,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub @VirtualDeviceParams.DynamicDisplayPolicyType int policyType, @VirtualDeviceParams.DevicePolicy int devicePolicy) { checkCallerIsDeviceOwner(); - if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) { + if (!Flags.activityControlApi()) { return; } synchronized (mVirtualDeviceLock) { @@ -1412,8 +1412,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub return mirroredDisplayId == Display.INVALID_DISPLAY ? displayId : mirroredDisplayId; } - @GuardedBy("mVirtualDeviceLock") - private GenericWindowPolicyController createWindowPolicyControllerLocked( + private GenericWindowPolicyController createWindowPolicyController( @NonNull Set<String> displayCategories) { final boolean activityLaunchAllowedByDefault = getDevicePolicy(POLICY_TYPE_ACTIVITY) == DEVICE_POLICY_DEFAULT; @@ -1422,28 +1421,28 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub final boolean showTasksInHostDeviceRecents = getDevicePolicy(POLICY_TYPE_RECENTS) == DEVICE_POLICY_DEFAULT; - if (mActivityListenerAdapter == null) { - mActivityListenerAdapter = new GwpcActivityListener(); - } - - final GenericWindowPolicyController gwpc = new GenericWindowPolicyController( - WindowManager.LayoutParams.FLAG_SECURE, - WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS, - mAttributionSource, - getAllowedUserHandles(), - activityLaunchAllowedByDefault, - mActivityPolicyExemptions, - mActivityPolicyPackageExemptions, - crossTaskNavigationAllowedByDefault, - /* crossTaskNavigationExemptions= */crossTaskNavigationAllowedByDefault - ? mParams.getBlockedCrossTaskNavigations() - : mParams.getAllowedCrossTaskNavigations(), - mActivityListenerAdapter, - displayCategories, - showTasksInHostDeviceRecents, - mParams.getHomeComponent()); - gwpc.registerRunningAppsChangedListener(/* listener= */ this); - return gwpc; + synchronized (mVirtualDeviceLock) { + if (mActivityListenerAdapter == null) { + mActivityListenerAdapter = new GwpcActivityListener(); + } + + return new GenericWindowPolicyController( + WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS, + mAttributionSource, + getAllowedUserHandles(), + activityLaunchAllowedByDefault, + mActivityPolicyExemptions, + mActivityPolicyPackageExemptions, + crossTaskNavigationAllowedByDefault, + /* crossTaskNavigationExemptions= */crossTaskNavigationAllowedByDefault + ? mParams.getBlockedCrossTaskNavigations() + : mParams.getAllowedCrossTaskNavigations(), + mActivityListenerAdapter, + displayCategories, + showTasksInHostDeviceRecents, + mParams.getHomeComponent()); + } } @Override // Binder call @@ -1451,55 +1450,54 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub @NonNull IVirtualDisplayCallback callback) { checkCallerIsDeviceOwner(); - int displayId; - boolean showPointer; - boolean isTrustedDisplay; - GenericWindowPolicyController gwpc; - synchronized (mVirtualDeviceLock) { - gwpc = createWindowPolicyControllerLocked(virtualDisplayConfig.getDisplayCategories()); - displayId = mDisplayManagerInternal.createVirtualDisplay(virtualDisplayConfig, + final boolean isTrustedDisplay = + (virtualDisplayConfig.getFlags() & DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED) + == DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED; + if (!isTrustedDisplay && getDevicePolicy(POLICY_TYPE_CLIPBOARD) != DEVICE_POLICY_DEFAULT) { + throw new SecurityException( + "All displays must be trusted for devices with custom clipboard policy."); + } + + GenericWindowPolicyController gwpc = + createWindowPolicyController(virtualDisplayConfig.getDisplayCategories()); + + // Create the display outside of the lock to avoid deadlock. DisplayManagerService will + // acquire the global WM lock while creating the display. At the same time, WM may query + // VDM and this virtual device to get policies, display ownership, etc. + int displayId = mDisplayManagerInternal.createVirtualDisplay(virtualDisplayConfig, callback, this, gwpc, mOwnerPackageName); - boolean isMirrorDisplay = - mDisplayManagerInternal.getDisplayIdToMirror(displayId) - != Display.INVALID_DISPLAY; - gwpc.setDisplayId(displayId, isMirrorDisplay); - isTrustedDisplay = - (mDisplayManagerInternal.getDisplayInfo(displayId).flags & Display.FLAG_TRUSTED) - == Display.FLAG_TRUSTED; - if (!isTrustedDisplay - && getDevicePolicy(POLICY_TYPE_CLIPBOARD) != DEVICE_POLICY_DEFAULT) { - throw new SecurityException("All displays must be trusted for devices with " - + "custom clipboard policy."); - } + if (displayId == Display.INVALID_DISPLAY) { + return displayId; + } - if (mVirtualDisplays.contains(displayId)) { - gwpc.unregisterRunningAppsChangedListener(this); - throw new IllegalStateException( - "Virtual device already has a virtual display with ID " + displayId); + // DisplayManagerService will call onVirtualDisplayCreated() after the display is created, + // while holding its own lock to ensure that this device knows about the display before any + // other display listeners are notified about the display creation. + VirtualDisplayWrapper displayWrapper; + boolean showPointer; + synchronized (mVirtualDeviceLock) { + if (!mVirtualDisplays.contains(displayId)) { + throw new IllegalStateException("Virtual device was not notified about the " + + "creation of display with ID " + displayId); } - - PowerManager.WakeLock wakeLock = - isTrustedDisplay ? createAndAcquireWakeLockForDisplay(displayId) : null; - mVirtualDisplays.put(displayId, new VirtualDisplayWrapper(callback, gwpc, wakeLock, - isTrustedDisplay, isMirrorDisplay)); + displayWrapper = mVirtualDisplays.get(displayId); showPointer = mDefaultShowPointerIcon; } + displayWrapper.acquireWakeLock(); + gwpc.registerRunningAppsChangedListener(/* listener= */ this); - final long token = Binder.clearCallingIdentity(); - try { + Binder.withCleanCallingIdentity(() -> { mInputController.setMouseScalingEnabled(false, displayId); mInputController.setDisplayEligibilityForPointerCapture(/* isEligible= */ false, displayId); - if (isTrustedDisplay) { + if (displayWrapper.isTrusted()) { mInputController.setShowPointerIcon(showPointer, displayId); mInputController.setDisplayImePolicy(displayId, WindowManager.DISPLAY_IME_POLICY_LOCAL); } else { gwpc.setShowInHostDeviceRecents(true); } - } finally { - Binder.restoreCallingIdentity(token); - } + }); Counter.logIncrementWithUid( "virtual_devices.value_virtual_display_created_count", @@ -1507,8 +1505,8 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub return displayId; } - private PowerManager.WakeLock createAndAcquireWakeLockForDisplay(int displayId) { - if (android.companion.virtualdevice.flags.Flags.deviceAwareDisplayPower()) { + private PowerManager.WakeLock createWakeLockForDisplay(int displayId) { + if (Flags.deviceAwareDisplayPower()) { return null; } final long token = Binder.clearCallingIdentity(); @@ -1517,7 +1515,6 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub PowerManager.WakeLock wakeLock = powerManager.newWakeLock( PowerManager.SCREEN_BRIGHT_WAKE_LOCK, TAG + ":" + displayId, displayId); - wakeLock.acquire(); return wakeLock; } finally { Binder.restoreCallingIdentity(token); @@ -1531,7 +1528,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub // infinite blocking loop. return false; } - if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) { + if (!Flags.activityControlApi()) { return true; } // Do not show the dialog if disabled by policy. @@ -1562,17 +1559,47 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub return result; } + /** + * DisplayManagerService is notifying this virtual device about the display creation. This + * should happen before the DisplayManagerInternal#createVirtualDisplay() call above + * returns. + * This is called while holding the DisplayManagerService lock, so no heavy-weight work must + * be done here and especially *** no calls to WindowManager! *** + */ + public void onVirtualDisplayCreated(int displayId, IVirtualDisplayCallback callback, + DisplayWindowPolicyController dwpc) { + final boolean isMirrorDisplay = + mDisplayManagerInternal.getDisplayIdToMirror(displayId) != Display.INVALID_DISPLAY; + final boolean isTrustedDisplay = + (mDisplayManagerInternal.getDisplayInfo(displayId).flags & Display.FLAG_TRUSTED) + == Display.FLAG_TRUSTED; + + GenericWindowPolicyController gwpc = (GenericWindowPolicyController) dwpc; + gwpc.setDisplayId(displayId, isMirrorDisplay); + PowerManager.WakeLock wakeLock = + isTrustedDisplay ? createWakeLockForDisplay(displayId) : null; + synchronized (mVirtualDeviceLock) { + if (mVirtualDisplays.contains(displayId)) { + Slog.wtf(TAG, "Virtual device already has a virtual display with ID " + displayId); + return; + } + mVirtualDisplays.put(displayId, new VirtualDisplayWrapper(callback, gwpc, wakeLock, + isTrustedDisplay, isMirrorDisplay)); + } + } + + /** + * This is callback invoked by VirtualDeviceManagerService when VirtualDisplay was released + * by DisplayManager (most probably caused by someone calling VirtualDisplay.close()). + * At this point, the display is already released, but we still need to release the + * corresponding wakeLock and unregister the RunningAppsChangedListener from corresponding + * WindowPolicyController. + * + * Note that when the display is destroyed during VirtualDeviceImpl.close() call, + * this callback won't be invoked because the display is removed from + * VirtualDeviceManagerService before any resources are released. + */ void onVirtualDisplayRemoved(int displayId) { - /* This is callback invoked by VirtualDeviceManagerService when VirtualDisplay was released - * by DisplayManager (most probably caused by someone calling VirtualDisplay.close()). - * At this point, the display is already released, but we still need to release the - * corresponding wakeLock and unregister the RunningAppsChangedListener from corresponding - * WindowPolicyController. - * - * Note that when the display is destroyed during VirtualDeviceImpl.close() call, - * this callback won't be invoked because the display is removed from - * VirtualDeviceManagerService before any resources are released. - */ VirtualDisplayWrapper virtualDisplayWrapper; synchronized (mVirtualDeviceLock) { virtualDisplayWrapper = mVirtualDisplays.removeReturnOld(displayId); @@ -1848,6 +1875,12 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub return mWindowPolicyController; } + void acquireWakeLock() { + if (mWakeLock != null && !mWakeLock.isHeld()) { + mWakeLock.acquire(); + } + } + void releaseWakeLock() { if (mWakeLock != null && mWakeLock.isHeld()) { mWakeLock.release(); @@ -1868,8 +1901,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } private static boolean isVirtualCameraEnabled() { - return Flags.virtualCamera() && virtualCameraServiceDiscovery() - && nativeVirtualCameraServiceBuildFlagEnabled(); + return nativeVirtualCameraServiceBuildFlagEnabled(); } // Returns true if virtual_camera service is enabled in this build. diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java index 8a0b85859b66..ff82ca00b840 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -40,7 +40,6 @@ import android.companion.virtual.IVirtualDeviceSoundEffectListener; import android.companion.virtual.VirtualDevice; import android.companion.virtual.VirtualDeviceManager; import android.companion.virtual.VirtualDeviceParams; -import android.companion.virtual.flags.Flags; import android.companion.virtual.sensor.VirtualSensor; import android.companion.virtualnative.IVirtualDeviceManagerNative; import android.compat.annotation.ChangeId; @@ -49,6 +48,7 @@ import android.content.AttributionSource; import android.content.Context; import android.content.Intent; import android.hardware.display.DisplayManagerInternal; +import android.hardware.display.IVirtualDisplayCallback; import android.os.Binder; import android.os.Build; import android.os.Handler; @@ -67,6 +67,7 @@ import android.util.Slog; import android.util.SparseArray; import android.view.Display; import android.widget.Toast; +import android.window.DisplayWindowPolicyController; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; @@ -751,6 +752,16 @@ public class VirtualDeviceManagerService extends SystemService { } @Override + public void onVirtualDisplayCreated(IVirtualDevice virtualDevice, int displayId, + IVirtualDisplayCallback callback, DisplayWindowPolicyController dwpc) { + VirtualDeviceImpl virtualDeviceImpl = getVirtualDeviceForId( + ((VirtualDeviceImpl) virtualDevice).getDeviceId()); + if (virtualDeviceImpl != null) { + virtualDeviceImpl.onVirtualDisplayCreated(displayId, callback, dwpc); + } + } + + @Override public void onVirtualDisplayRemoved(IVirtualDevice virtualDevice, int displayId) { VirtualDeviceImpl virtualDeviceImpl = getVirtualDeviceForId( ((VirtualDeviceImpl) virtualDevice).getDeviceId()); diff --git a/services/core/Android.bp b/services/core/Android.bp index f98076ab41e4..00db11e72dd9 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -292,9 +292,18 @@ java_genrule { out: ["services.core.priorityboosted.jar"], } +java_genrule_combiner { + name: "services.core.combined", + static_libs: ["services.core.priorityboosted"], + headers: ["services.core.unboosted"], +} + java_library { name: "services.core", - static_libs: ["services.core.priorityboosted"], + static_libs: select(release_flag("RELEASE_SERVICES_JAVA_GENRULE_COMBINER"), { + true: ["services.core.combined"], + default: ["services.core.priorityboosted"], + }), } java_library_host { diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index 350ecab1dd5f..d2a5734f323f 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -163,6 +163,10 @@ import com.android.server.pm.UserManagerInternal; import com.android.server.storage.AppFuseBridge; import com.android.server.storage.StorageSessionController; import com.android.server.storage.StorageSessionController.ExternalStorageServiceException; +import com.android.server.storage.WatchedVolumeInfo; +import com.android.server.utils.Watchable; +import com.android.server.utils.WatchedArrayMap; +import com.android.server.utils.Watcher; import com.android.server.wm.ActivityTaskManagerInternal; import com.android.server.wm.ActivityTaskManagerInternal.ScreenObserver; @@ -452,7 +456,7 @@ class StorageManagerService extends IStorageManager.Stub private ArrayMap<String, DiskInfo> mDisks = new ArrayMap<>(); /** Map from volume ID to disk */ @GuardedBy("mLock") - private final ArrayMap<String, VolumeInfo> mVolumes = new ArrayMap<>(); + private final WatchedArrayMap<String, WatchedVolumeInfo> mVolumes = new WatchedArrayMap<>(); /** Map from UUID to record */ @GuardedBy("mLock") @@ -503,9 +507,9 @@ class StorageManagerService extends IStorageManager.Stub "(?i)(^/storage/[^/]+/(?:([0-9]+)/)?Android/(?:data|media|obb|sandbox)/)([^/]+)(/.*)?"); - private VolumeInfo findVolumeByIdOrThrow(String id) { + private WatchedVolumeInfo findVolumeByIdOrThrow(String id) { synchronized (mLock) { - final VolumeInfo vol = mVolumes.get(id); + final WatchedVolumeInfo vol = mVolumes.get(id); if (vol != null) { return vol; } @@ -516,9 +520,9 @@ class StorageManagerService extends IStorageManager.Stub private VolumeRecord findRecordForPath(String path) { synchronized (mLock) { for (int i = 0; i < mVolumes.size(); i++) { - final VolumeInfo vol = mVolumes.valueAt(i); - if (vol.path != null && path.startsWith(vol.path)) { - return mRecords.get(vol.fsUuid); + final WatchedVolumeInfo vol = mVolumes.valueAt(i); + if (vol.getFsPath() != null && path.startsWith(vol.getFsPath())) { + return mRecords.get(vol.getFsUuid()); } } } @@ -764,7 +768,7 @@ class StorageManagerService extends IStorageManager.Stub break; } case H_VOLUME_MOUNT: { - final VolumeInfo vol = (VolumeInfo) msg.obj; + final WatchedVolumeInfo vol = (WatchedVolumeInfo) msg.obj; if (isMountDisallowed(vol)) { Slog.i(TAG, "Ignoring mount " + vol.getId() + " due to policy"); break; @@ -774,7 +778,7 @@ class StorageManagerService extends IStorageManager.Stub break; } case H_VOLUME_UNMOUNT: { - final VolumeInfo vol = (VolumeInfo) msg.obj; + final WatchedVolumeInfo vol = (WatchedVolumeInfo) msg.obj; unmount(vol); break; } @@ -828,7 +832,8 @@ class StorageManagerService extends IStorageManager.Stub } case H_VOLUME_STATE_CHANGED: { final SomeArgs args = (SomeArgs) msg.obj; - onVolumeStateChangedAsync((VolumeInfo) args.arg1, args.argi1, args.argi2); + onVolumeStateChangedAsync((WatchedVolumeInfo) args.arg1, args.argi1, + args.argi2); args.recycle(); break; } @@ -892,9 +897,9 @@ class StorageManagerService extends IStorageManager.Stub synchronized (mLock) { final int size = mVolumes.size(); for (int i = 0; i < size; i++) { - final VolumeInfo vol = mVolumes.valueAt(i); - if (vol.mountUserId == userId) { - vol.mountUserId = UserHandle.USER_NULL; + final WatchedVolumeInfo vol = mVolumes.valueAt(i); + if (vol.getMountUserId() == userId) { + vol.setMountUserId(UserHandle.USER_NULL); mHandler.obtainMessage(H_VOLUME_UNMOUNT, vol).sendToTarget(); } } @@ -1084,7 +1089,7 @@ class StorageManagerService extends IStorageManager.Stub VolumeInfo.TYPE_PRIVATE, null, null); internal.state = VolumeInfo.STATE_MOUNTED; internal.path = Environment.getDataDirectory().getAbsolutePath(); - mVolumes.put(internal.id, internal); + mVolumes.put(internal.id, WatchedVolumeInfo.fromVolumeInfo(internal)); } private void resetIfBootedAndConnected() { @@ -1242,7 +1247,7 @@ class StorageManagerService extends IStorageManager.Stub } } for (int i = 0; i < mVolumes.size(); i++) { - final VolumeInfo vol = mVolumes.valueAt(i); + final WatchedVolumeInfo vol = mVolumes.valueAt(i); if (vol.isVisibleForUser(userId) && vol.isMountedReadable()) { final StorageVolume userVol = vol.buildStorageVolume(mContext, userId, false); mHandler.obtainMessage(H_VOLUME_BROADCAST, userVol).sendToTarget(); @@ -1291,21 +1296,21 @@ class StorageManagerService extends IStorageManager.Stub } private void maybeRemountVolumes(int userId) { - List<VolumeInfo> volumesToRemount = new ArrayList<>(); + List<WatchedVolumeInfo> volumesToRemount = new ArrayList<>(); synchronized (mLock) { for (int i = 0; i < mVolumes.size(); i++) { - final VolumeInfo vol = mVolumes.valueAt(i); + final WatchedVolumeInfo vol = mVolumes.valueAt(i); if (!vol.isPrimary() && vol.isMountedWritable() && vol.isVisible() && vol.getMountUserId() != mCurrentUserId) { // If there's a visible secondary volume mounted, // we need to update the currentUserId and remount - vol.mountUserId = mCurrentUserId; + vol.setMountUserId(mCurrentUserId); volumesToRemount.add(vol); } } } - for (VolumeInfo vol : volumesToRemount) { + for (WatchedVolumeInfo vol : volumesToRemount) { Slog.i(TAG, "Remounting volume for user: " + userId + ". Volume: " + vol); mHandler.obtainMessage(H_VOLUME_UNMOUNT, vol).sendToTarget(); mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget(); @@ -1317,12 +1322,12 @@ class StorageManagerService extends IStorageManager.Stub * trying to mount doesn't have the same mount user id as the current user being maintained by * StorageManagerService and change the mount Id. The checks are same as * {@link StorageManagerService#maybeRemountVolumes(int)} - * @param VolumeInfo object to consider for changing the mountId + * @param vol {@link WatchedVolumeInfo} object to consider for changing the mountId */ - private void updateVolumeMountIdIfRequired(VolumeInfo vol) { + private void updateVolumeMountIdIfRequired(WatchedVolumeInfo vol) { synchronized (mLock) { if (!vol.isPrimary() && vol.isVisible() && vol.getMountUserId() != mCurrentUserId) { - vol.mountUserId = mCurrentUserId; + vol.setMountUserId(mCurrentUserId); } } } @@ -1485,20 +1490,21 @@ class StorageManagerService extends IStorageManager.Stub final DiskInfo disk = mDisks.get(diskId); final VolumeInfo vol = new VolumeInfo(volId, type, disk, partGuid); vol.mountUserId = userId; - mVolumes.put(volId, vol); - onVolumeCreatedLocked(vol); + WatchedVolumeInfo watchedVol = WatchedVolumeInfo.fromVolumeInfo(vol); + mVolumes.put(volId, watchedVol); + onVolumeCreatedLocked(watchedVol); } } @Override public void onVolumeStateChanged(String volId, final int newState, final int userId) { synchronized (mLock) { - final VolumeInfo vol = mVolumes.get(volId); + final WatchedVolumeInfo vol = mVolumes.get(volId); if (vol != null) { - final int oldState = vol.state; - vol.state = newState; - final VolumeInfo vInfo = new VolumeInfo(vol); - vInfo.mountUserId = userId; + final int oldState = vol.getState(); + vol.setState(newState); + final WatchedVolumeInfo vInfo = new WatchedVolumeInfo(vol); + vInfo.setMountUserId(userId); final SomeArgs args = SomeArgs.obtain(); args.arg1 = vInfo; args.argi1 = oldState; @@ -1513,11 +1519,11 @@ class StorageManagerService extends IStorageManager.Stub public void onVolumeMetadataChanged(String volId, String fsType, String fsUuid, String fsLabel) { synchronized (mLock) { - final VolumeInfo vol = mVolumes.get(volId); + final WatchedVolumeInfo vol = mVolumes.get(volId); if (vol != null) { - vol.fsType = fsType; - vol.fsUuid = fsUuid; - vol.fsLabel = fsLabel; + vol.setFsType(fsType); + vol.setFsUuid(fsUuid); + vol.setFsLabel(fsLabel); } } } @@ -1525,9 +1531,9 @@ class StorageManagerService extends IStorageManager.Stub @Override public void onVolumePathChanged(String volId, String path) { synchronized (mLock) { - final VolumeInfo vol = mVolumes.get(volId); + final WatchedVolumeInfo vol = mVolumes.get(volId); if (vol != null) { - vol.path = path; + vol.setFsPath(path); } } } @@ -1535,24 +1541,24 @@ class StorageManagerService extends IStorageManager.Stub @Override public void onVolumeInternalPathChanged(String volId, String internalPath) { synchronized (mLock) { - final VolumeInfo vol = mVolumes.get(volId); + final WatchedVolumeInfo vol = mVolumes.get(volId); if (vol != null) { - vol.internalPath = internalPath; + vol.setInternalPath(internalPath); } } } @Override public void onVolumeDestroyed(String volId) { - VolumeInfo vol = null; + WatchedVolumeInfo vol = null; synchronized (mLock) { vol = mVolumes.remove(volId); } if (vol != null) { - mStorageSessionController.onVolumeRemove(vol); + mStorageSessionController.onVolumeRemove(vol.getImmutableVolumeInfo()); try { - if (vol.type == VolumeInfo.TYPE_PRIVATE) { + if (vol.getType() == VolumeInfo.TYPE_PRIVATE) { mInstaller.onPrivateVolumeRemoved(vol.getFsUuid()); } } catch (Installer.InstallerException e) { @@ -1566,7 +1572,7 @@ class StorageManagerService extends IStorageManager.Stub private void onDiskScannedLocked(DiskInfo disk) { int volumeCount = 0; for (int i = 0; i < mVolumes.size(); i++) { - final VolumeInfo vol = mVolumes.valueAt(i); + final WatchedVolumeInfo vol = mVolumes.valueAt(i); if (Objects.equals(disk.id, vol.getDiskId())) { volumeCount++; } @@ -1589,19 +1595,19 @@ class StorageManagerService extends IStorageManager.Stub } @GuardedBy("mLock") - private void onVolumeCreatedLocked(VolumeInfo vol) { + private void onVolumeCreatedLocked(WatchedVolumeInfo vol) { final ActivityManagerInternal amInternal = LocalServices.getService(ActivityManagerInternal.class); - if (vol.mountUserId >= 0 && !amInternal.isUserRunning(vol.mountUserId, 0)) { + if (vol.getMountUserId() >= 0 && !amInternal.isUserRunning(vol.getMountUserId(), 0)) { Slog.d(TAG, "Ignoring volume " + vol.getId() + " because user " - + Integer.toString(vol.mountUserId) + " is no longer running."); + + Integer.toString(vol.getMountUserId()) + " is no longer running."); return; } - if (vol.type == VolumeInfo.TYPE_EMULATED) { + if (vol.getType() == VolumeInfo.TYPE_EMULATED) { final Context volumeUserContext = mContext.createContextAsUser( - UserHandle.of(vol.mountUserId), 0); + UserHandle.of(vol.getMountUserId()), 0); boolean isMediaSharedWithParent = (volumeUserContext != null) ? volumeUserContext.getSystemService( @@ -1611,60 +1617,60 @@ class StorageManagerService extends IStorageManager.Stub // should not be skipped even if media provider instance is not running in that user // space if (!isMediaSharedWithParent - && !mStorageSessionController.supportsExternalStorage(vol.mountUserId)) { + && !mStorageSessionController.supportsExternalStorage(vol.getMountUserId())) { Slog.d(TAG, "Ignoring volume " + vol.getId() + " because user " - + Integer.toString(vol.mountUserId) + + Integer.toString(vol.getMountUserId()) + " does not support external storage."); return; } final StorageManager storage = mContext.getSystemService(StorageManager.class); - final VolumeInfo privateVol = storage.findPrivateForEmulated(vol); + final VolumeInfo privateVol = storage.findPrivateForEmulated(vol.getVolumeInfo()); if ((Objects.equals(StorageManager.UUID_PRIVATE_INTERNAL, mPrimaryStorageUuid) && VolumeInfo.ID_PRIVATE_INTERNAL.equals(privateVol.id)) - || Objects.equals(privateVol.fsUuid, mPrimaryStorageUuid)) { + || Objects.equals(privateVol.getFsUuid(), mPrimaryStorageUuid)) { Slog.v(TAG, "Found primary storage at " + vol); - vol.mountFlags |= VolumeInfo.MOUNT_FLAG_PRIMARY; - vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE; + vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_PRIMARY); + vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE); mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget(); } - } else if (vol.type == VolumeInfo.TYPE_PUBLIC) { + } else if (vol.getType() == VolumeInfo.TYPE_PUBLIC) { // TODO: only look at first public partition if (Objects.equals(StorageManager.UUID_PRIMARY_PHYSICAL, mPrimaryStorageUuid) - && vol.disk.isDefaultPrimary()) { + && vol.getDisk().isDefaultPrimary()) { Slog.v(TAG, "Found primary storage at " + vol); - vol.mountFlags |= VolumeInfo.MOUNT_FLAG_PRIMARY; - vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE; + vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_PRIMARY); + vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE); } // Adoptable public disks are visible to apps, since they meet // public API requirement of being in a stable location. - if (vol.disk.isAdoptable()) { - vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE; + if (vol.getDisk().isAdoptable()) { + vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE); } - vol.mountUserId = mCurrentUserId; + vol.setMountUserId(mCurrentUserId); mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget(); - } else if (vol.type == VolumeInfo.TYPE_PRIVATE) { + } else if (vol.getType() == VolumeInfo.TYPE_PRIVATE) { mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget(); - } else if (vol.type == VolumeInfo.TYPE_STUB) { - if (vol.disk.isStubVisible()) { - vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE; + } else if (vol.getType() == VolumeInfo.TYPE_STUB) { + if (vol.getDisk().isStubVisible()) { + vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE); } else { - vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_READ; + vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_READ); } - vol.mountUserId = mCurrentUserId; + vol.setMountUserId(mCurrentUserId); mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget(); } else { Slog.d(TAG, "Skipping automatic mounting of " + vol); } } - private boolean isBroadcastWorthy(VolumeInfo vol) { + private boolean isBroadcastWorthy(WatchedVolumeInfo vol) { switch (vol.getType()) { case VolumeInfo.TYPE_PRIVATE: case VolumeInfo.TYPE_PUBLIC: @@ -1691,8 +1697,8 @@ class StorageManagerService extends IStorageManager.Stub } @GuardedBy("mLock") - private void onVolumeStateChangedLocked(VolumeInfo vol, int newState) { - if (vol.type == VolumeInfo.TYPE_EMULATED) { + private void onVolumeStateChangedLocked(WatchedVolumeInfo vol, int newState) { + if (vol.getType() == VolumeInfo.TYPE_EMULATED) { if (newState != VolumeInfo.STATE_MOUNTED) { mFuseMountedUser.remove(vol.getMountUserId()); } else if (mVoldAppDataIsolationEnabled){ @@ -1741,7 +1747,7 @@ class StorageManagerService extends IStorageManager.Stub } } - private void onVolumeStateChangedAsync(VolumeInfo vol, int oldState, int newState) { + private void onVolumeStateChangedAsync(WatchedVolumeInfo vol, int oldState, int newState) { if (newState == VolumeInfo.STATE_MOUNTED) { // Private volumes can be unmounted and re-mounted even after a user has // been unlocked; on devices that support encryption keys tied to the filesystem, @@ -1751,7 +1757,7 @@ class StorageManagerService extends IStorageManager.Stub } catch (Exception e) { // Unusable partition, unmount. try { - mVold.unmount(vol.id); + mVold.unmount(vol.getId()); } catch (Exception ee) { Slog.wtf(TAG, ee); } @@ -1762,20 +1768,20 @@ class StorageManagerService extends IStorageManager.Stub synchronized (mLock) { // Remember that we saw this volume so we're ready to accept user // metadata, or so we can annoy them when a private volume is ejected - if (!TextUtils.isEmpty(vol.fsUuid)) { - VolumeRecord rec = mRecords.get(vol.fsUuid); + if (!TextUtils.isEmpty(vol.getFsUuid())) { + VolumeRecord rec = mRecords.get(vol.getFsUuid()); if (rec == null) { - rec = new VolumeRecord(vol.type, vol.fsUuid); - rec.partGuid = vol.partGuid; + rec = new VolumeRecord(vol.getType(), vol.getFsUuid()); + rec.partGuid = vol.getPartGuid(); rec.createdMillis = System.currentTimeMillis(); - if (vol.type == VolumeInfo.TYPE_PRIVATE) { - rec.nickname = vol.disk.getDescription(); + if (vol.getType() == VolumeInfo.TYPE_PRIVATE) { + rec.nickname = vol.getDisk().getDescription(); } mRecords.put(rec.fsUuid, rec); } else { // Handle upgrade case where we didn't store partition GUID if (TextUtils.isEmpty(rec.partGuid)) { - rec.partGuid = vol.partGuid; + rec.partGuid = vol.getPartGuid(); } } @@ -1788,7 +1794,7 @@ class StorageManagerService extends IStorageManager.Stub // before notifying other listeners. // Intentionally called without the mLock to avoid deadlocking from the Storage Service. try { - mStorageSessionController.notifyVolumeStateChanged(vol); + mStorageSessionController.notifyVolumeStateChanged(vol.getImmutableVolumeInfo()); } catch (ExternalStorageServiceException e) { Log.e(TAG, "Failed to notify volume state changed to the Storage Service", e); } @@ -1799,9 +1805,9 @@ class StorageManagerService extends IStorageManager.Stub // processes that receive the intent unnecessarily. if (mBootCompleted && isBroadcastWorthy(vol)) { final Intent intent = new Intent(VolumeInfo.ACTION_VOLUME_STATE_CHANGED); - intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.id); + intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId()); intent.putExtra(VolumeInfo.EXTRA_VOLUME_STATE, newState); - intent.putExtra(VolumeRecord.EXTRA_FS_UUID, vol.fsUuid); + intent.putExtra(VolumeRecord.EXTRA_FS_UUID, vol.getFsUuid()); intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); mHandler.obtainMessage(H_INTERNAL_BROADCAST, intent).sendToTarget(); @@ -1826,8 +1832,8 @@ class StorageManagerService extends IStorageManager.Stub } } - if ((vol.type == VolumeInfo.TYPE_PUBLIC || vol.type == VolumeInfo.TYPE_STUB) - && vol.state == VolumeInfo.STATE_EJECTING) { + if ((vol.getType() == VolumeInfo.TYPE_PUBLIC || vol.getType() == VolumeInfo.TYPE_STUB) + && vol.getState() == VolumeInfo.STATE_EJECTING) { // TODO: this should eventually be handled by new ObbVolume state changes /* * Some OBBs might have been unmounted when this volume was @@ -1835,7 +1841,7 @@ class StorageManagerService extends IStorageManager.Stub * remove those from the list of mounted OBBS. */ mObbActionHandler.sendMessage(mObbActionHandler.obtainMessage( - OBB_FLUSH_MOUNT_STATE, vol.path)); + OBB_FLUSH_MOUNT_STATE, vol.getFsPath())); } maybeLogMediaMount(vol, newState); } @@ -1860,7 +1866,7 @@ class StorageManagerService extends IStorageManager.Stub } } - private void maybeLogMediaMount(VolumeInfo vol, int newState) { + private void maybeLogMediaMount(WatchedVolumeInfo vol, int newState) { if (!SecurityLog.isLoggingEnabled()) { return; } @@ -1875,10 +1881,10 @@ class StorageManagerService extends IStorageManager.Stub if (newState == VolumeInfo.STATE_MOUNTED || newState == VolumeInfo.STATE_MOUNTED_READ_ONLY) { - SecurityLog.writeEvent(SecurityLog.TAG_MEDIA_MOUNT, vol.path, label); + SecurityLog.writeEvent(SecurityLog.TAG_MEDIA_MOUNT, vol.getFsPath(), label); } else if (newState == VolumeInfo.STATE_UNMOUNTED || newState == VolumeInfo.STATE_BAD_REMOVAL) { - SecurityLog.writeEvent(SecurityLog.TAG_MEDIA_UNMOUNT, vol.path, label); + SecurityLog.writeEvent(SecurityLog.TAG_MEDIA_UNMOUNT, vol.getFsPath(), label); } } @@ -1920,18 +1926,18 @@ class StorageManagerService extends IStorageManager.Stub /** * Decide if volume is mountable per device policies. */ - private boolean isMountDisallowed(VolumeInfo vol) { + private boolean isMountDisallowed(WatchedVolumeInfo vol) { UserManager userManager = mContext.getSystemService(UserManager.class); boolean isUsbRestricted = false; - if (vol.disk != null && vol.disk.isUsb()) { + if (vol.getDisk() != null && vol.getDisk().isUsb()) { isUsbRestricted = userManager.hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER, Binder.getCallingUserHandle()); } boolean isTypeRestricted = false; - if (vol.type == VolumeInfo.TYPE_PUBLIC || vol.type == VolumeInfo.TYPE_PRIVATE - || vol.type == VolumeInfo.TYPE_STUB) { + if (vol.getType() == VolumeInfo.TYPE_PUBLIC || vol.getType() == VolumeInfo.TYPE_PRIVATE + || vol.getType() == VolumeInfo.TYPE_STUB) { isTypeRestricted = userManager .hasUserRestriction(UserManager.DISALLOW_MOUNT_PHYSICAL_MEDIA, Binder.getCallingUserHandle()); @@ -1967,6 +1973,13 @@ class StorageManagerService extends IStorageManager.Stub mContext = context; mCallbacks = new Callbacks(FgThread.get().getLooper()); + mVolumes.registerObserver(new Watcher() { + @Override + public void onChange(Watchable what) { + // When we change the list or any volume contained in it, invalidate the cache + StorageManager.invalidateVolumeListCache(); + } + }); HandlerThread hthread = new HandlerThread(TAG); hthread.start(); mHandler = new StorageManagerServiceHandler(hthread.getLooper()); @@ -2339,7 +2352,7 @@ class StorageManagerService extends IStorageManager.Stub super.mount_enforcePermission(); - final VolumeInfo vol = findVolumeByIdOrThrow(volId); + final WatchedVolumeInfo vol = findVolumeByIdOrThrow(volId); if (isMountDisallowed(vol)) { throw new SecurityException("Mounting " + volId + " restricted by policy"); } @@ -2365,23 +2378,24 @@ class StorageManagerService extends IStorageManager.Stub } } - private void mount(VolumeInfo vol) { + private void mount(WatchedVolumeInfo vol) { try { - Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "SMS.mount: " + vol.id); + Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "SMS.mount: " + vol.getId()); // TODO(b/135341433): Remove cautious logging when FUSE is stable Slog.i(TAG, "Mounting volume " + vol); extendWatchdogTimeout("#mount might be slow"); - mVold.mount(vol.id, vol.mountFlags, vol.mountUserId, new IVoldMountCallback.Stub() { + mVold.mount(vol.getId(), vol.getMountFlags(), vol.getMountUserId(), + new IVoldMountCallback.Stub() { @Override public boolean onVolumeChecking(FileDescriptor fd, String path, String internalPath) { - vol.path = path; - vol.internalPath = internalPath; + vol.setFsPath(path); + vol.setInternalPath(internalPath); ParcelFileDescriptor pfd = new ParcelFileDescriptor(fd); try { Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, - "SMS.startFuseFileSystem: " + vol.id); - mStorageSessionController.onVolumeMount(pfd, vol); + "SMS.startFuseFileSystem: " + vol.getId()); + mStorageSessionController.onVolumeMount(pfd, vol.getImmutableVolumeInfo()); return true; } catch (ExternalStorageServiceException e) { Slog.e(TAG, "Failed to mount volume " + vol, e); @@ -2416,21 +2430,21 @@ class StorageManagerService extends IStorageManager.Stub super.unmount_enforcePermission(); - final VolumeInfo vol = findVolumeByIdOrThrow(volId); + final WatchedVolumeInfo vol = findVolumeByIdOrThrow(volId); unmount(vol); } - private void unmount(VolumeInfo vol) { + private void unmount(WatchedVolumeInfo vol) { try { try { - if (vol.type == VolumeInfo.TYPE_PRIVATE) { + if (vol.getType() == VolumeInfo.TYPE_PRIVATE) { mInstaller.onPrivateVolumeRemoved(vol.getFsUuid()); } } catch (Installer.InstallerException e) { Slog.e(TAG, "Failed unmount mirror data", e); } - mVold.unmount(vol.id); - mStorageSessionController.onVolumeUnmount(vol); + mVold.unmount(vol.getId()); + mStorageSessionController.onVolumeUnmount(vol.getImmutableVolumeInfo()); } catch (Exception e) { Slog.wtf(TAG, e); } @@ -2442,10 +2456,10 @@ class StorageManagerService extends IStorageManager.Stub super.format_enforcePermission(); - final VolumeInfo vol = findVolumeByIdOrThrow(volId); - final String fsUuid = vol.fsUuid; + final WatchedVolumeInfo vol = findVolumeByIdOrThrow(volId); + final String fsUuid = vol.getFsUuid(); try { - mVold.format(vol.id, "auto"); + mVold.format(vol.getId(), "auto"); // After a successful format above, we should forget about any // records for the old partition, since it'll never appear again @@ -3105,7 +3119,7 @@ class StorageManagerService extends IStorageManager.Stub private void warnOnNotMounted() { synchronized (mLock) { for (int i = 0; i < mVolumes.size(); i++) { - final VolumeInfo vol = mVolumes.valueAt(i); + final WatchedVolumeInfo vol = mVolumes.valueAt(i); if (vol.isPrimary() && vol.isMountedWritable()) { // Cool beans, we have a mounted primary volume return; @@ -3392,8 +3406,8 @@ class StorageManagerService extends IStorageManager.Stub } } - private void prepareUserStorageIfNeeded(VolumeInfo vol) throws Exception { - if (vol.type != VolumeInfo.TYPE_PRIVATE) { + private void prepareUserStorageIfNeeded(WatchedVolumeInfo vol) throws Exception { + if (vol.getType() != VolumeInfo.TYPE_PRIVATE) { return; } @@ -3411,7 +3425,7 @@ class StorageManagerService extends IStorageManager.Stub continue; } - prepareUserStorageInternal(vol.fsUuid, user.id, flags); + prepareUserStorageInternal(vol.getFsUuid(), user.id, flags); } } @@ -3960,7 +3974,7 @@ class StorageManagerService extends IStorageManager.Stub synchronized (mLock) { for (int i = 0; i < mVolumes.size(); i++) { final String volId = mVolumes.keyAt(i); - final VolumeInfo vol = mVolumes.valueAt(i); + final WatchedVolumeInfo vol = mVolumes.valueAt(i); switch (vol.getType()) { case VolumeInfo.TYPE_PUBLIC: case VolumeInfo.TYPE_STUB: @@ -4112,7 +4126,7 @@ class StorageManagerService extends IStorageManager.Stub synchronized (mLock) { final VolumeInfo[] res = new VolumeInfo[mVolumes.size()]; for (int i = 0; i < mVolumes.size(); i++) { - res[i] = mVolumes.valueAt(i); + res[i] = mVolumes.valueAt(i).getVolumeInfo(); } return res; } @@ -4708,7 +4722,8 @@ class StorageManagerService extends IStorageManager.Stub break; } case MSG_VOLUME_STATE_CHANGED: { - callback.onVolumeStateChanged((VolumeInfo) args.arg1, args.argi2, args.argi3); + VolumeInfo volInfo = ((WatchedVolumeInfo) args.arg1).getVolumeInfo(); + callback.onVolumeStateChanged(volInfo, args.argi2, args.argi3); break; } case MSG_VOLUME_RECORD_CHANGED: { @@ -4738,7 +4753,7 @@ class StorageManagerService extends IStorageManager.Stub obtainMessage(MSG_STORAGE_STATE_CHANGED, args).sendToTarget(); } - private void notifyVolumeStateChanged(VolumeInfo vol, int oldState, int newState) { + private void notifyVolumeStateChanged(WatchedVolumeInfo vol, int oldState, int newState) { final SomeArgs args = SomeArgs.obtain(); args.arg1 = vol.clone(); args.argi2 = oldState; @@ -4790,8 +4805,8 @@ class StorageManagerService extends IStorageManager.Stub pw.println("Volumes:"); pw.increaseIndent(); for (int i = 0; i < mVolumes.size(); i++) { - final VolumeInfo vol = mVolumes.valueAt(i); - if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(vol.id)) continue; + final WatchedVolumeInfo vol = mVolumes.valueAt(i); + if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(vol.getId())) continue; vol.dump(pw); } pw.decreaseIndent(); @@ -5088,7 +5103,7 @@ class StorageManagerService extends IStorageManager.Stub final List<String> primaryVolumeIds = new ArrayList<>(); synchronized (mLock) { for (int i = 0; i < mVolumes.size(); i++) { - final VolumeInfo vol = mVolumes.valueAt(i); + final WatchedVolumeInfo vol = mVolumes.valueAt(i); if (vol.isPrimary()) { primaryVolumeIds.add(vol.getId()); } diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 6cca7d16842a..cce29592d912 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -8302,8 +8302,6 @@ public final class ActiveServices { if ((allowWiu == REASON_DENIED) || (allowStart == REASON_DENIED)) { @ReasonCode final int allowWhileInUse = shouldAllowFgsWhileInUsePermissionLocked( callingPackage, callingPid, callingUid, r.app, backgroundStartPrivileges); - // We store them to compare the old and new while-in-use logics to each other. - // (They're not used for any other purposes.) if (allowWiu == REASON_DENIED) { allowWiu = allowWhileInUse; } @@ -8706,6 +8704,7 @@ public final class ActiveServices { + ",duration:" + tempAllowListReason.mDuration + ",callingUid:" + tempAllowListReason.mCallingUid)) + ">" + + "; allowWiu:" + allowWhileInUse + "; targetSdkVersion:" + r.appInfo.targetSdkVersion + "; callerTargetSdkVersion:" + callerTargetSdkVersion + "; startForegroundCount:" + r.mStartForegroundCount diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index c8b0a57fe9f0..5ff6999e40b3 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -3705,8 +3705,14 @@ public final class BatteryStatsService extends IBatteryStats.Stub @Override public void takeUidSnapshotsAsync(int[] requestUids, ResultReceiver resultReceiver) { if (!onlyCaller(requestUids)) { - mContext.enforceCallingOrSelfPermission( - android.Manifest.permission.BATTERY_STATS, null); + try { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.BATTERY_STATS, null); + } catch (SecurityException ex) { + resultReceiver.send(IBatteryStats.RESULT_SECURITY_EXCEPTION, + Bundle.forPair(IBatteryStats.KEY_EXCEPTION_MESSAGE, ex.getMessage())); + return; + } } if (shouldCollectExternalStats()) { @@ -3727,13 +3733,14 @@ public final class BatteryStatsService extends IBatteryStats.Stub } Bundle resultData = new Bundle(1); resultData.putParcelableArray(IBatteryStats.KEY_UID_SNAPSHOTS, results); - resultReceiver.send(0, resultData); + resultReceiver.send(IBatteryStats.RESULT_OK, resultData); } catch (Exception ex) { if (DBG) { Slog.d(TAG, "Crashed while returning results for takeUidSnapshots(" + Arrays.toString(requestUids) + ") i=" + i, ex); } - throw ex; + resultReceiver.send(IBatteryStats.RESULT_RUNTIME_EXCEPTION, + Bundle.forPair(IBatteryStats.KEY_EXCEPTION_MESSAGE, ex.getMessage())); } finally { Binder.restoreCallingIdentity(ident); } diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index c6338307b192..f1007e75e0af 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -186,9 +186,28 @@ public class SettingsToPropertiesMapper { "core_libraries", "crumpet", "dck_framework", + "desktop_apps", + "desktop_better_together", + "desktop_bsp", + "desktop_camera", "desktop_connectivity", + "desktop_display", + "desktop_commercial", + "desktop_firmware", + "desktop_graphics", "desktop_hwsec", + "desktop_input", + "desktop_kernel", + "desktop_ml", + "desktop_serviceability", + "desktop_oobe", + "desktop_peripherals", + "desktop_pnp", + "desktop_security", "desktop_stats", + "desktop_sysui", + "desktop_users_and_accounts", + "desktop_video", "desktop_wifi", "devoptions_settings", "game", diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index e0fbaf43ea43..27e9e44f1090 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -31,7 +31,6 @@ import static android.app.ActivityManagerInternal.ALLOW_FULL_ONLY; import static android.app.ActivityManagerInternal.ALLOW_NON_FULL; import static android.app.ActivityManagerInternal.ALLOW_NON_FULL_IN_PROFILE; import static android.app.ActivityManagerInternal.ALLOW_PROFILES_OR_NON_FULL; -import static android.app.KeyguardManager.LOCK_ON_USER_SWITCH_CALLBACK; import static android.os.PowerWhitelistManager.REASON_BOOT_COMPLETED; import static android.os.PowerWhitelistManager.REASON_LOCKED_BOOT_COMPLETED; import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED; @@ -3905,6 +3904,10 @@ class UserController implements Handler.Callback { return mService.mWindowManager; } + ActivityTaskManagerInternal getActivityTaskManagerInternal() { + return mService.mAtmInternal; + } + void activityManagerOnUserStopped(@UserIdInt int userId) { LocalServices.getService(ActivityTaskManagerInternal.class).onUserStopped(userId); } @@ -4119,25 +4122,40 @@ class UserController implements Handler.Callback { } void lockDeviceNowAndWaitForKeyguardShown() { + if (getWindowManager().isKeyguardLocked()) { + Slogf.w(TAG, "Not locking the device since the keyguard is already locked"); + return; + } + final TimingsTraceAndSlog t = new TimingsTraceAndSlog(); t.traceBegin("lockDeviceNowAndWaitForKeyguardShown"); final CountDownLatch latch = new CountDownLatch(1); - Bundle bundle = new Bundle(); - bundle.putBinder(LOCK_ON_USER_SWITCH_CALLBACK, new IRemoteCallback.Stub() { - public void sendResult(Bundle data) { - latch.countDown(); - } - }); - getWindowManager().lockNow(bundle); + ActivityTaskManagerInternal.ScreenObserver screenObserver = + new ActivityTaskManagerInternal.ScreenObserver() { + @Override + public void onAwakeStateChanged(boolean isAwake) { + + } + + @Override + public void onKeyguardStateChanged(boolean isShowing) { + if (isShowing) { + latch.countDown(); + } + } + }; + + getActivityTaskManagerInternal().registerScreenObserver(screenObserver); + getWindowManager().lockDeviceNow(); try { if (!latch.await(20, TimeUnit.SECONDS)) { - throw new RuntimeException("User controller expected a callback while waiting " - + "to show the keyguard. Timed out after 20 seconds."); + throw new RuntimeException("Keyguard is not shown in 20 seconds"); } } catch (InterruptedException e) { throw new RuntimeException(e); } finally { + getActivityTaskManagerInternal().unregisterScreenObserver(screenObserver); t.traceEnd(); } } diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index 32c4e9b1727e..2a9762caaf79 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -72,6 +72,7 @@ import static android.content.Intent.EXTRA_REPLACING; import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS; import static android.content.pm.PermissionInfo.PROTECTION_FLAG_APPOP; import static android.os.Flags.binderFrozenStateChangeCallback; +import static android.permission.flags.Flags.appOpsServiceHandlerFix; import static android.permission.flags.Flags.checkOpValidatePackage; import static android.permission.flags.Flags.deviceAwareAppOpNewSchemaEnabled; import static android.permission.flags.Flags.useFrozenAwareRemoteCallbackList; @@ -174,6 +175,7 @@ import com.android.internal.util.XmlUtils; import com.android.internal.util.function.pooled.PooledLambda; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; +import com.android.server.IoThread; import com.android.server.LocalManagerRegistry; import com.android.server.LocalServices; import com.android.server.LockGuard; @@ -277,6 +279,7 @@ public class AppOpsService extends IAppOpsService.Stub { final AtomicFile mStorageFile; final AtomicFile mRecentAccessesFile; private final @Nullable File mNoteOpCallerStacktracesFile; + /* AMS handler, this shouldn't be used for IO */ final Handler mHandler; private final AppOpsRecentAccessPersistence mRecentAccessPersistence; @@ -1411,7 +1414,7 @@ public class AppOpsService extends IAppOpsService.Stub { @GuardedBy("this") private void packageRemovedLocked(int uid, String packageName) { - mHandler.post(PooledLambda.obtainRunnable(HistoricalRegistry::clearHistory, + getIoHandler().post(PooledLambda.obtainRunnable(HistoricalRegistry::clearHistory, mHistoricalRegistry, uid, packageName)); UidState uidState = mUidStates.get(uid); @@ -1693,7 +1696,7 @@ public class AppOpsService extends IAppOpsService.Stub { if (mWriteScheduled) { mWriteScheduled = false; mFastWriteScheduled = false; - mHandler.removeCallbacks(mWriteRunner); + getIoHandler().removeCallbacks(mWriteRunner); doWrite = true; } } @@ -1979,7 +1982,7 @@ public class AppOpsService extends IAppOpsService.Stub { new String[attributionChainExemptPackages.size()]) : null; // Must not hold the appops lock - mHandler.post(PooledLambda.obtainRunnable(HistoricalRegistry::getHistoricalOps, + getIoHandler().post(PooledLambda.obtainRunnable(HistoricalRegistry::getHistoricalOps, mHistoricalRegistry, uid, packageName, attributionTag, opNamesArray, dataType, filter, beginTimeMillis, endTimeMillis, flags, chainExemptPkgArray, callback).recycleOnUse()); @@ -2010,7 +2013,8 @@ public class AppOpsService extends IAppOpsService.Stub { new String[attributionChainExemptPackages.size()]) : null; // Must not hold the appops lock - mHandler.post(PooledLambda.obtainRunnable(HistoricalRegistry::getHistoricalOpsFromDiskRaw, + getIoHandler().post(PooledLambda.obtainRunnable( + HistoricalRegistry::getHistoricalOpsFromDiskRaw, mHistoricalRegistry, uid, packageName, attributionTag, opNamesArray, dataType, filter, beginTimeMillis, endTimeMillis, flags, chainExemptPkgArray, callback).recycleOnUse()); @@ -5074,7 +5078,7 @@ public class AppOpsService extends IAppOpsService.Stub { private void scheduleWriteLocked() { if (!mWriteScheduled) { mWriteScheduled = true; - mHandler.postDelayed(mWriteRunner, WRITE_DELAY); + getIoHandler().postDelayed(mWriteRunner, WRITE_DELAY); } } @@ -5082,8 +5086,8 @@ public class AppOpsService extends IAppOpsService.Stub { if (!mFastWriteScheduled) { mWriteScheduled = true; mFastWriteScheduled = true; - mHandler.removeCallbacks(mWriteRunner); - mHandler.postDelayed(mWriteRunner, 10*1000); + getIoHandler().removeCallbacks(mWriteRunner); + getIoHandler().postDelayed(mWriteRunner, 10 * 1000); } } @@ -5957,7 +5961,8 @@ public class AppOpsService extends IAppOpsService.Stub { final long token = Binder.clearCallingIdentity(); try { synchronized (shell.mInternal) { - shell.mInternal.mHandler.removeCallbacks(shell.mInternal.mWriteRunner); + shell.mInternal.getIoHandler().removeCallbacks( + shell.mInternal.mWriteRunner); } shell.mInternal.writeRecentAccesses(); shell.mInternal.mAppOpsCheckingService.writeState(); @@ -7884,4 +7889,12 @@ public class AppOpsService extends IAppOpsService.Stub { return null; } } + + private Handler getIoHandler() { + if (appOpsServiceHandlerFix()) { + return IoThread.getHandler(); + } else { + return mHandler; + } + } } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 2219ecc77167..b48d0a6ed547 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -66,7 +66,6 @@ import static com.android.media.audio.Flags.equalScoLeaVcIndexRange; import static com.android.media.audio.Flags.replaceStreamBtSco; import static com.android.media.audio.Flags.ringMyCar; import static com.android.media.audio.Flags.ringerModeAffectsAlarm; -import static com.android.media.audio.Flags.vgsVssSyncMuteOrder; import static com.android.media.flags.Flags.enableAudioInputDeviceRoutingAndVolumeControl; import static com.android.server.audio.SoundDoseHelper.ACTION_CHECK_MUSIC_ACTIVE; import static com.android.server.utils.EventLogger.Event.ALOGE; @@ -4977,9 +4976,8 @@ public class AudioService extends IAudioService.Stub + roForegroundAudioControl()); pw.println("\tandroid.media.audio.scoManagedByAudio:" + scoManagedByAudio()); - pw.println("\tcom.android.media.audio.vgsVssSyncMuteOrder:" - + vgsVssSyncMuteOrder()); pw.println("\tcom.android.media.audio.absVolumeIndexFix - EOL"); + pw.println("\tcom.android.media.audio.vgsVssSyncMuteOrder - EOL"); pw.println("\tcom.android.media.audio.replaceStreamBtSco:" + replaceStreamBtSco()); pw.println("\tcom.android.media.audio.equalScoLeaVcIndexRange:" @@ -9010,22 +9008,13 @@ public class AudioService extends IAudioService.Stub synced = true; continue; } - if (vgsVssSyncMuteOrder()) { - if ((isMuted() != streamMuted) && isVssMuteBijective( - stream)) { - vss.mute(isMuted(), "VGS.applyAllVolumes#1"); - } + if ((isMuted() != streamMuted) && isVssMuteBijective(stream)) { + vss.mute(isMuted(), "VGS.applyAllVolumes#1"); } if (indexForStream != index) { vss.setIndex(index * 10, device, caller, true /*hasModifyAudioSettings*/); } - if (!vgsVssSyncMuteOrder()) { - if ((isMuted() != streamMuted) && isVssMuteBijective( - stream)) { - vss.mute(isMuted(), "VGS.applyAllVolumes#1"); - } - } } } } @@ -15093,11 +15082,13 @@ public class AudioService extends IAudioService.Stub final String key = "additional_output_device_delay"; final String reply = AudioSystem.getParameters( key + "=" + device.getInternalType() + "," + device.getAddress()); - long delayMillis; - try { - delayMillis = Long.parseLong(reply.substring(key.length() + 1)); - } catch (NullPointerException e) { - delayMillis = 0; + long delayMillis = 0; + if (reply.contains(key)) { + try { + delayMillis = Long.parseLong(reply.substring(key.length() + 1)); + } catch (NullPointerException e) { + delayMillis = 0; + } } return delayMillis; } @@ -15123,11 +15114,13 @@ public class AudioService extends IAudioService.Stub final String key = "max_additional_output_device_delay"; final String reply = AudioSystem.getParameters( key + "=" + device.getInternalType() + "," + device.getAddress()); - long delayMillis; - try { - delayMillis = Long.parseLong(reply.substring(key.length() + 1)); - } catch (NullPointerException e) { - delayMillis = 0; + long delayMillis = 0; + if (reply.contains(key)) { + try { + delayMillis = Long.parseLong(reply.substring(key.length() + 1)); + } catch (NullPointerException e) { + delayMillis = 0; + } } return delayMillis; } diff --git a/services/core/java/com/android/server/backup/InputBackupHelper.java b/services/core/java/com/android/server/backup/InputBackupHelper.java new file mode 100644 index 000000000000..af9606c6e70f --- /dev/null +++ b/services/core/java/com/android/server/backup/InputBackupHelper.java @@ -0,0 +1,82 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.backup; + +import static com.android.server.input.InputManagerInternal.BACKUP_CATEGORY_INPUT_GESTURES; + +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.app.backup.BlobBackupHelper; +import android.util.Slog; + +import com.android.server.LocalServices; +import com.android.server.input.InputManagerInternal; + +import java.util.HashMap; +import java.util.Map; + +public class InputBackupHelper extends BlobBackupHelper { + private static final String TAG = "InputBackupHelper"; // must be < 23 chars + + // Current version of the blob schema + private static final int BLOB_VERSION = 1; + + // Key under which the payload blob is stored + private static final String KEY_INPUT_GESTURES = "input_gestures"; + + private final @UserIdInt int mUserId; + + private final @NonNull InputManagerInternal mInputManagerInternal; + + public InputBackupHelper(int userId) { + super(BLOB_VERSION, KEY_INPUT_GESTURES); + mUserId = userId; + mInputManagerInternal = LocalServices.getService(InputManagerInternal.class); + } + + @Override + protected byte[] getBackupPayload(String key) { + Map<Integer, byte[]> payloads; + try { + payloads = mInputManagerInternal.getBackupPayload(mUserId); + } catch (Exception exception) { + Slog.e(TAG, "Failed to get backup payload for input gestures", exception); + return null; + } + + if (KEY_INPUT_GESTURES.equals(key)) { + return payloads.getOrDefault(BACKUP_CATEGORY_INPUT_GESTURES, null); + } + + return null; + } + + @Override + protected void applyRestoredPayload(String key, byte[] payload) { + Map<Integer, byte[]> payloads = new HashMap<>(); + if (KEY_INPUT_GESTURES.equals(key)) { + payloads.put(BACKUP_CATEGORY_INPUT_GESTURES, payload); + } + + try { + mInputManagerInternal.applyBackupPayload(payloads, mUserId); + } catch (Exception exception) { + Slog.e(TAG, "Failed to apply input backup payload", exception); + } + } + +} diff --git a/services/core/java/com/android/server/backup/SystemBackupAgent.java b/services/core/java/com/android/server/backup/SystemBackupAgent.java index 677e0c055455..b11267ef8634 100644 --- a/services/core/java/com/android/server/backup/SystemBackupAgent.java +++ b/services/core/java/com/android/server/backup/SystemBackupAgent.java @@ -68,6 +68,7 @@ public class SystemBackupAgent extends BackupAgentHelper { private static final String COMPANION_HELPER = "companion"; private static final String SYSTEM_GENDER_HELPER = "system_gender"; private static final String DISPLAY_HELPER = "display"; + private static final String INPUT_HELPER = "input"; // These paths must match what the WallpaperManagerService uses. The leaf *_FILENAME // are also used in the full-backup file format, so must not change unless steps are @@ -112,7 +113,7 @@ public class SystemBackupAgent extends BackupAgentHelper { private static final Set<String> sEligibleHelpersForNonSystemUser = SetUtils.union(sEligibleHelpersForProfileUser, Sets.newArraySet(ACCOUNT_MANAGER_HELPER, USAGE_STATS_HELPER, PREFERRED_HELPER, - SHORTCUT_MANAGER_HELPER)); + SHORTCUT_MANAGER_HELPER, INPUT_HELPER)); private int mUserId = UserHandle.USER_SYSTEM; private boolean mIsProfileUser = false; @@ -149,6 +150,9 @@ public class SystemBackupAgent extends BackupAgentHelper { addHelperIfEligibleForUser(SYSTEM_GENDER_HELPER, new SystemGrammaticalGenderBackupHelper(mUserId)); addHelperIfEligibleForUser(DISPLAY_HELPER, new DisplayBackupHelper(mUserId)); + if (com.android.hardware.input.Flags.enableBackupAndRestoreForInputGestures()) { + addHelperIfEligibleForUser(INPUT_HELPER, new InputBackupHelper(mUserId)); + } } @Override diff --git a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java index 471b7b4ddfc8..d412277d2605 100644 --- a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java +++ b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java @@ -24,8 +24,10 @@ import android.companion.virtual.VirtualDeviceManager; import android.companion.virtual.VirtualDeviceParams; import android.companion.virtual.sensor.VirtualSensor; import android.content.Context; +import android.hardware.display.IVirtualDisplayCallback; import android.os.LocaleList; import android.util.ArraySet; +import android.window.DisplayWindowPolicyController; import java.util.Set; import java.util.function.Consumer; @@ -104,6 +106,17 @@ public abstract class VirtualDeviceManagerInternal { public abstract @NonNull ArraySet<Integer> getDeviceIdsForUid(int uid); /** + * Notifies that a virtual display was created. + * + * @param virtualDevice The virtual device that owns the virtual display. + * @param displayId The display id of the created virtual display. + * @param callback The callback of the virtual display. + * @param dwpc The DisplayWindowPolicyController of the created virtual display. + */ + public abstract void onVirtualDisplayCreated(IVirtualDevice virtualDevice, int displayId, + IVirtualDisplayCallback callback, DisplayWindowPolicyController dwpc); + + /** * Notifies that a virtual display is removed. * * @param virtualDevice The virtual device where the virtual display located. diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index e83efc573ea8..854b0dd7676b 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -2041,6 +2041,7 @@ public final class DisplayManagerService extends SystemService { packageName, displayUniqueId, virtualDevice, + dwpc, surface, flags, virtualDisplayConfig); @@ -2135,6 +2136,7 @@ public final class DisplayManagerService extends SystemService { String packageName, String uniqueId, IVirtualDevice virtualDevice, + DisplayWindowPolicyController dwpc, Surface surface, int flags, VirtualDisplayConfig virtualDisplayConfig) { @@ -2188,6 +2190,16 @@ public final class DisplayManagerService extends SystemService { final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(device); if (display != null) { + // Notify the virtual device that the display has been created. This needs to be called + // in this locked section before the repository had the chance to notify any listeners + // to ensure that the device is aware of the new display before others know about it. + if (virtualDevice != null) { + final VirtualDeviceManagerInternal vdm = + getLocalService(VirtualDeviceManagerInternal.class); + vdm.onVirtualDisplayCreated( + virtualDevice, display.getDisplayIdLocked(), callback, dwpc); + } + return display.getDisplayIdLocked(); } diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java index b49c01b3e2a8..83ca563e0534 100644 --- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java +++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java @@ -781,6 +781,11 @@ final class LocalDisplayAdapter extends DisplayAdapter { if (isDisplayPrivate(physicalAddress)) { mInfo.flags |= DisplayDeviceInfo.FLAG_PRIVATE; } + + if (isDisplayStealTopFocusDisabled(physicalAddress)) { + mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_FOCUS; + mInfo.flags |= DisplayDeviceInfo.FLAG_STEAL_TOP_FOCUS_DISABLED; + } } if (DisplayCutout.getMaskBuiltInDisplayCutout(res, mInfo.uniqueId)) { @@ -1467,6 +1472,23 @@ final class LocalDisplayAdapter extends DisplayAdapter { } return false; } + + private boolean isDisplayStealTopFocusDisabled(DisplayAddress.Physical physicalAddress) { + if (physicalAddress == null) { + return false; + } + final Resources res = getOverlayContext().getResources(); + int[] ports = res.getIntArray(R.array.config_localNotStealTopFocusDisplayPorts); + if (ports != null) { + int port = physicalAddress.getPort(); + for (int p : ports) { + if (p == port) { + return true; + } + } + } + return false; + } } private boolean hdrTypesEqual(int[] modeHdrTypes, int[] recordHdrTypes) { diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java index a1e8f08db0a6..aab2760dbc66 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -122,11 +122,6 @@ public class DisplayManagerFlags { Flags.FLAG_ALWAYS_ROTATE_DISPLAY_DEVICE, Flags::alwaysRotateDisplayDevice); - private final FlagState mRefreshRateVotingTelemetry = new FlagState( - Flags.FLAG_REFRESH_RATE_VOTING_TELEMETRY, - Flags::refreshRateVotingTelemetry - ); - private final FlagState mPixelAnisotropyCorrectionEnabled = new FlagState( Flags.FLAG_ENABLE_PIXEL_ANISOTROPY_CORRECTION, Flags::enablePixelAnisotropyCorrection @@ -403,10 +398,6 @@ public class DisplayManagerFlags { return mAlwaysRotateDisplayDevice.isEnabled(); } - public boolean isRefreshRateVotingTelemetryEnabled() { - return mRefreshRateVotingTelemetry.isEnabled(); - } - public boolean isPixelAnisotropyCorrectionInLogicalDisplayEnabled() { return mPixelAnisotropyCorrectionEnabled.isEnabled(); } @@ -626,7 +617,6 @@ public class DisplayManagerFlags { pw.println(" " + mAutoBrightnessModesFlagState); pw.println(" " + mFastHdrTransitions); pw.println(" " + mAlwaysRotateDisplayDevice); - pw.println(" " + mRefreshRateVotingTelemetry); pw.println(" " + mPixelAnisotropyCorrectionEnabled); pw.println(" " + mSensorBasedBrightnessThrottling); pw.println(" " + mIdleScreenRefreshRateTimeout); diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index cc0bbde370fe..8211febade60 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -191,14 +191,6 @@ flag { } flag { - name: "refresh_rate_voting_telemetry" - namespace: "display_manager" - description: "Feature flag for enabling telemetry for refresh rate voting in DisplayManager" - bug: "310029108" - is_fixed_read_only: true -} - -flag { name: "enable_pixel_anisotropy_correction" namespace: "display_manager" description: "Feature flag for enabling display anisotropy correction through LogicalDisplay upscaling" diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java index 1dd4a9b93277..c37733b05fba 100644 --- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java +++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java @@ -229,8 +229,7 @@ public class DisplayModeDirector { mContext = context; mHandler = new DisplayModeDirectorHandler(handler.getLooper()); mInjector = injector; - mVotesStatsReporter = injector.getVotesStatsReporter( - displayManagerFlags.isRefreshRateVotingTelemetryEnabled()); + mVotesStatsReporter = injector.getVotesStatsReporter(); mSupportedModesByDisplay = new SparseArray<>(); mAppSupportedModesByDisplay = new SparseArray<>(); mDefaultModeByDisplay = new SparseArray<>(); @@ -3141,7 +3140,7 @@ public class DisplayModeDirector { SensorManagerInternal getSensorManagerInternal(); @Nullable - VotesStatsReporter getVotesStatsReporter(boolean refreshRateVotingTelemetryEnabled); + VotesStatsReporter getVotesStatsReporter(); } @VisibleForTesting @@ -3281,10 +3280,9 @@ public class DisplayModeDirector { } @Override - public VotesStatsReporter getVotesStatsReporter(boolean refreshRateVotingTelemetryEnabled) { + public VotesStatsReporter getVotesStatsReporter() { // if frame rate override supported, renderRates will be ignored in mode selection - return new VotesStatsReporter(supportsFrameRateOverride(), - refreshRateVotingTelemetryEnabled); + return new VotesStatsReporter(supportsFrameRateOverride()); } private DisplayManager getDisplayManager() { diff --git a/services/core/java/com/android/server/display/mode/VotesStatsReporter.java b/services/core/java/com/android/server/display/mode/VotesStatsReporter.java index 7562a525b5f6..7b579c0e0c21 100644 --- a/services/core/java/com/android/server/display/mode/VotesStatsReporter.java +++ b/services/core/java/com/android/server/display/mode/VotesStatsReporter.java @@ -25,6 +25,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.os.Trace; import android.util.SparseArray; +import android.util.SparseIntArray; import android.view.Display; import com.android.internal.util.FrameworkStatsLog; @@ -36,13 +37,11 @@ class VotesStatsReporter { private static final String TAG = "VotesStatsReporter"; private static final int REFRESH_RATE_NOT_LIMITED = 1000; private final boolean mIgnoredRenderRate; - private final boolean mFrameworkStatsLogReportingEnabled; - private int mLastMinPriorityReported = Vote.MAX_PRIORITY + 1; + private final SparseIntArray mLastMinPriorityByDisplay = new SparseIntArray(); - public VotesStatsReporter(boolean ignoreRenderRate, boolean refreshRateVotingTelemetryEnabled) { + VotesStatsReporter(boolean ignoreRenderRate) { mIgnoredRenderRate = ignoreRenderRate; - mFrameworkStatsLogReportingEnabled = refreshRateVotingTelemetryEnabled; } void reportVoteChanged(int displayId, int priority, @Nullable Vote vote) { @@ -57,32 +56,27 @@ class VotesStatsReporter { int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate); Trace.traceCounter(Trace.TRACE_TAG_POWER, TAG + "." + displayId + ":" + Vote.priorityToString(priority), maxRefreshRate); - if (mFrameworkStatsLogReportingEnabled) { - FrameworkStatsLog.write( - DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority, - DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ADDED, - maxRefreshRate, -1); - } + FrameworkStatsLog.write( + DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority, + DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ADDED, + maxRefreshRate, -1); } private void reportVoteRemoved(int displayId, int priority) { Trace.traceCounter(Trace.TRACE_TAG_POWER, TAG + "." + displayId + ":" + Vote.priorityToString(priority), -1); - if (mFrameworkStatsLogReportingEnabled) { - FrameworkStatsLog.write( - DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority, - DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_REMOVED, -1, -1); - } + FrameworkStatsLog.write( + DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority, + DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_REMOVED, -1, -1); } void reportVotesActivated(int displayId, int minPriority, @Nullable Display.Mode baseMode, SparseArray<Vote> votes) { - if (!mFrameworkStatsLogReportingEnabled) { - return; - } + int lastMinPriorityReported = mLastMinPriorityByDisplay.get( + displayId, Vote.MAX_PRIORITY + 1); int selectedRefreshRate = baseMode != null ? (int) baseMode.getRefreshRate() : -1; for (int priority = Vote.MIN_PRIORITY; priority <= Vote.MAX_PRIORITY; priority++) { - if (priority < mLastMinPriorityReported && priority < minPriority) { + if (priority < lastMinPriorityReported && priority < minPriority) { continue; } Vote vote = votes.get(priority); @@ -91,7 +85,7 @@ class VotesStatsReporter { } // Was previously reported ACTIVE, changed to ADDED - if (priority >= mLastMinPriorityReported && priority < minPriority) { + if (priority >= lastMinPriorityReported && priority < minPriority) { int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate); FrameworkStatsLog.write( DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority, @@ -99,7 +93,7 @@ class VotesStatsReporter { maxRefreshRate, selectedRefreshRate); } // Was previously reported ADDED, changed to ACTIVE - if (priority >= minPriority && priority < mLastMinPriorityReported) { + if (priority >= minPriority && priority < lastMinPriorityReported) { int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate); FrameworkStatsLog.write( DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority, @@ -107,7 +101,7 @@ class VotesStatsReporter { maxRefreshRate, selectedRefreshRate); } - mLastMinPriorityReported = minPriority; + mLastMinPriorityByDisplay.put(displayId, minPriority); } } diff --git a/services/core/java/com/android/server/flags/services.aconfig b/services/core/java/com/android/server/flags/services.aconfig index 4505d0e2d799..7e5c1bc9ada5 100644 --- a/services/core/java/com/android/server/flags/services.aconfig +++ b/services/core/java/com/android/server/flags/services.aconfig @@ -86,3 +86,10 @@ flag { description: "Enable the time notifications feature, a toggle to enable/disable time-related notifications in Date & Time Settings" bug: "283267917" } + +flag { + name: "certpininstaller_removal" + namespace: "network_security" + description: "Remove CertPinInstallReceiver from the platform" + bug: "391205997" +} diff --git a/services/core/java/com/android/server/input/InputDataStore.java b/services/core/java/com/android/server/input/InputDataStore.java index e8f21fe8fb74..834f8154240e 100644 --- a/services/core/java/com/android/server/input/InputDataStore.java +++ b/services/core/java/com/android/server/input/InputDataStore.java @@ -125,8 +125,20 @@ public final class InputDataStore { } } - @VisibleForTesting - List<InputGestureData> readInputGesturesXml(InputStream stream, boolean utf8Encoded) + /** + * Parses the given input stream and returns the list of {@link InputGestureData} objects. + * This parsing happens on a best effort basis. If invalid data exists in the given payload + * it will be skipped. An example of this would be a keycode that does not exist in the + * present version of Android. If the payload is malformed, instead this will throw an + * exception and require the caller to handel this appropriately for its situation. + * + * @param stream stream of the input payload of XML data + * @param utf8Encoded whether or not the input data is UTF-8 encoded + * @return list of {@link InputGestureData} objects pulled from the payload + * @throws XmlPullParserException + * @throws IOException + */ + public List<InputGestureData> readInputGesturesXml(InputStream stream, boolean utf8Encoded) throws XmlPullParserException, IOException { List<InputGestureData> inputGestureDataList = new ArrayList<>(); TypedXmlPullParser parser; @@ -153,6 +165,31 @@ public final class InputDataStore { return inputGestureDataList; } + /** + * Serializes the given list of {@link InputGestureData} objects to XML in the provided output + * stream. + * + * @param stream output stream to put serialized data. + * @param utf8Encoded whether or not to encode the serialized data in UTF-8 format. + * @param inputGestureDataList the list of {@link InputGestureData} objects to serialize. + */ + public void writeInputGestureXml(OutputStream stream, boolean utf8Encoded, + List<InputGestureData> inputGestureDataList) throws IOException { + final TypedXmlSerializer serializer; + if (utf8Encoded) { + serializer = Xml.newFastSerializer(); + serializer.setOutput(stream, StandardCharsets.UTF_8.name()); + } else { + serializer = Xml.resolveSerializer(stream); + } + + serializer.startDocument(null, true); + serializer.startTag(null, TAG_ROOT); + writeInputGestureListToXml(serializer, inputGestureDataList); + serializer.endTag(null, TAG_ROOT); + serializer.endDocument(); + } + private InputGestureData readInputGestureFromXml(TypedXmlPullParser parser) throws XmlPullParserException, IOException, IllegalArgumentException { InputGestureData.Builder builder = new InputGestureData.Builder(); @@ -239,24 +276,6 @@ public final class InputDataStore { return inputGestureDataList; } - @VisibleForTesting - void writeInputGestureXml(OutputStream stream, boolean utf8Encoded, - List<InputGestureData> inputGestureDataList) throws IOException { - final TypedXmlSerializer serializer; - if (utf8Encoded) { - serializer = Xml.newFastSerializer(); - serializer.setOutput(stream, StandardCharsets.UTF_8.name()); - } else { - serializer = Xml.resolveSerializer(stream); - } - - serializer.startDocument(null, true); - serializer.startTag(null, TAG_ROOT); - writeInputGestureListToXml(serializer, inputGestureDataList); - serializer.endTag(null, TAG_ROOT); - serializer.endDocument(); - } - private void writeInputGestureToXml(TypedXmlSerializer serializer, InputGestureData inputGestureData) throws IOException { serializer.startTag(null, TAG_INPUT_GESTURE); diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java index d2486fe8bd66..87f693cc7291 100644 --- a/services/core/java/com/android/server/input/InputManagerInternal.java +++ b/services/core/java/com/android/server/input/InputManagerInternal.java @@ -16,6 +16,7 @@ package com.android.server.input; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; @@ -32,7 +33,11 @@ import android.view.inputmethod.InputMethodSubtype; import com.android.internal.inputmethod.InputMethodSubtypeHandle; import com.android.internal.policy.IShortcutService; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; import java.util.List; +import java.util.Map; /** * Input manager local system service interface. @@ -41,6 +46,15 @@ import java.util.List; */ public abstract class InputManagerInternal { + // Backup and restore information for custom input gestures. + public static final int BACKUP_CATEGORY_INPUT_GESTURES = 0; + + // Backup and Restore categories for sending map of data back and forth to backup and restore + // infrastructure. + @IntDef({BACKUP_CATEGORY_INPUT_GESTURES}) + public @interface BackupCategory { + } + /** * Called by the display manager to set information about the displays as needed * by the input system. The input system must copy this information to retain it. @@ -312,4 +326,22 @@ public abstract class InputManagerInternal { * @return true if setting power wakeup was successful. */ public abstract boolean setKernelWakeEnabled(int deviceId, boolean enabled); + + /** + * Retrieves the input gestures backup payload data. + * + * @param userId the user ID of the backup data. + * @return byte array of UTF-8 encoded backup data. + */ + public abstract Map<Integer, byte[]> getBackupPayload(int userId) throws IOException; + + /** + * Applies the given UTF-8 encoded byte array payload to the given user's input data + * on a best effort basis. + * + * @param payload UTF-8 encoded map of byte arrays of restored data + * @param userId the user ID for which to apply the payload data + */ + public abstract void applyBackupPayload(Map<Integer, byte[]> payload, int userId) + throws XmlPullParserException, IOException; } diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 2ad5a1538da9..4a5f4a19893a 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -24,6 +24,7 @@ import static android.provider.DeviceConfig.NAMESPACE_INPUT_NATIVE_BOOT; import static android.view.KeyEvent.KEYCODE_UNKNOWN; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; +import static com.android.hardware.input.Flags.enableCustomizableInputGestures; import static com.android.hardware.input.Flags.touchpadVisualizer; import static com.android.hardware.input.Flags.keyEventActivityDetection; import static com.android.hardware.input.Flags.useKeyGestureEventHandler; @@ -153,6 +154,8 @@ import com.android.server.wm.WindowManagerInternal; import libcore.io.IoUtils; +import org.xmlpull.v1.XmlPullParserException; + import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; @@ -3805,6 +3808,26 @@ public class InputManagerService extends IInputManager.Stub public boolean setKernelWakeEnabled(int deviceId, boolean enabled) { return mNative.setKernelWakeEnabled(deviceId, enabled); } + + @Override + public Map<Integer, byte[]> getBackupPayload(int userId) throws IOException { + final Map<Integer, byte[]> payload = new HashMap<>(); + if (enableCustomizableInputGestures()) { + payload.put(BACKUP_CATEGORY_INPUT_GESTURES, + mKeyGestureController.getInputGestureBackupPayload(userId)); + } + return payload; + } + + @Override + public void applyBackupPayload(Map<Integer, byte[]> payload, int userId) + throws XmlPullParserException, IOException { + if (enableCustomizableInputGestures() && payload.containsKey( + BACKUP_CATEGORY_INPUT_GESTURES)) { + mKeyGestureController.applyInputGesturesBackupPayload( + payload.get(BACKUP_CATEGORY_INPUT_GESTURES), userId); + } + } } @Override diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java index 41f58ae76a4d..5770a09e3b92 100644 --- a/services/core/java/com/android/server/input/KeyGestureController.java +++ b/services/core/java/com/android/server/input/KeyGestureController.java @@ -69,6 +69,11 @@ import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; import com.android.server.policy.KeyCombinationManager; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.util.ArrayDeque; import java.util.HashSet; import java.util.List; @@ -1191,6 +1196,29 @@ final class KeyGestureController { } } + byte[] getInputGestureBackupPayload(int userId) throws IOException { + final List<InputGestureData> inputGestureDataList = + mInputGestureManager.getCustomInputGestures(userId, null); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + synchronized (mInputDataStore) { + mInputDataStore.writeInputGestureXml(byteArrayOutputStream, true, inputGestureDataList); + } + return byteArrayOutputStream.toByteArray(); + } + + void applyInputGesturesBackupPayload(byte[] payload, int userId) + throws XmlPullParserException, IOException { + final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload); + List<InputGestureData> inputGestureDataList; + synchronized (mInputDataStore) { + inputGestureDataList = mInputDataStore.readInputGesturesXml(byteArrayInputStream, true); + } + for (final InputGestureData inputGestureData : inputGestureDataList) { + mInputGestureManager.addCustomInputGesture(userId, inputGestureData); + } + mHandler.obtainMessage(MSG_PERSIST_CUSTOM_GESTURES, userId).sendToTarget(); + } + // A record of a registered key gesture event listener from one process. private class KeyGestureEventListenerRecord implements IBinder.DeathRecipient { public final int mPid; diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java index ddace179348c..a04ffdb4951d 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java @@ -43,7 +43,6 @@ import com.android.internal.annotations.GuardedBy; import java.util.Collection; import java.util.Optional; -import java.util.concurrent.atomic.AtomicBoolean; /** * A class that represents a broker for the endpoint registered by the client app. This class @@ -89,8 +88,11 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub /** The remote callback interface for this endpoint. */ private final IContextHubEndpointCallback mContextHubEndpointCallback; - /** True if this endpoint is registered with the service. */ - private AtomicBoolean mIsRegistered = new AtomicBoolean(true); + /** True if this endpoint is registered with the service/HAL. */ + @GuardedBy("mRegistrationLock") + private boolean mIsRegistered = false; + + private final Object mRegistrationLock = new Object(); private final Object mOpenSessionLock = new Object(); @@ -192,7 +194,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub public int openSession(HubEndpointInfo destination, String serviceDescriptor) throws RemoteException { super.openSession_enforcePermission(); - if (!mIsRegistered.get()) throw new IllegalStateException("Endpoint is not registered"); + if (!isRegistered()) throw new IllegalStateException("Endpoint is not registered"); if (!hasEndpointPermissions(destination)) { throw new SecurityException( "Insufficient permission to open a session with endpoint: " + destination); @@ -223,7 +225,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void closeSession(int sessionId, int reason) throws RemoteException { super.closeSession_enforcePermission(); - if (!mIsRegistered.get()) throw new IllegalStateException("Endpoint is not registered"); + if (!isRegistered()) throw new IllegalStateException("Endpoint is not registered"); if (!cleanupSessionResources(sessionId)) { throw new IllegalArgumentException( "Unknown session ID in closeSession: id=" + sessionId); @@ -235,19 +237,26 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void unregister() { super.unregister_enforcePermission(); - mIsRegistered.set(false); - try { - mHubInterface.unregisterEndpoint(mHalEndpointInfo); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException while calling HAL unregisterEndpoint", e); - } synchronized (mOpenSessionLock) { // Iterate in reverse since cleanupSessionResources will remove the entry for (int i = mSessionInfoMap.size() - 1; i >= 0; i--) { int id = mSessionInfoMap.keyAt(i); + halCloseEndpointSessionNoThrow(id, Reason.ENDPOINT_GONE); cleanupSessionResources(id); } } + synchronized (mRegistrationLock) { + if (!isRegistered()) { + Log.w(TAG, "Attempting to unregister when already unregistered"); + return; + } + mIsRegistered = false; + try { + mHubInterface.unregisterEndpoint(mHalEndpointInfo); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException while calling HAL unregisterEndpoint", e); + } + } mEndpointManager.unregisterEndpoint(mEndpointInfo.getIdentifier().getEndpoint()); releaseWakeLockOnExit(); } @@ -335,7 +344,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub /** Invoked when the underlying binder of this broker has died at the client process. */ @Override public void binderDied() { - if (mIsRegistered.get()) { + if (isRegistered()) { unregister(); } } @@ -365,6 +374,22 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } } + /** + * Registers this endpoints with the Context Hub HAL. + * + * @throws RemoteException if the registrations fails with a RemoteException + */ + /* package */ void register() throws RemoteException { + synchronized (mRegistrationLock) { + if (isRegistered()) { + Log.w(TAG, "Attempting to register when already registered"); + } else { + mHubInterface.registerEndpoint(mHalEndpointInfo); + mIsRegistered = true; + } + } + } + /* package */ void attachDeathRecipient() throws RemoteException { if (mContextHubEndpointCallback != null) { mContextHubEndpointCallback.asBinder().linkToDeath(this, 0 /* flags */); @@ -425,6 +450,24 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } } + /* package */ void onHalRestart() { + synchronized (mRegistrationLock) { + mIsRegistered = false; + try { + register(); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException while calling HAL registerEndpoint", e); + } + } + synchronized (mOpenSessionLock) { + for (int i = mSessionInfoMap.size() - 1; i >= 0; i--) { + int id = mSessionInfoMap.keyAt(i); + onCloseEndpointSession(id, Reason.HUB_RESET); + } + } + // TODO(b/390029594): Cancel any ongoing reliable communication transactions + } + private Optional<Byte> onEndpointSessionOpenRequestInternal( int sessionId, HubEndpointInfo initiator, String serviceDescriptor) { if (!hasEndpointPermissions(initiator)) { @@ -553,7 +596,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub private void acquireWakeLock() { Binder.withCleanCallingIdentity( () -> { - if (mIsRegistered.get()) { + if (isRegistered()) { mWakeLock.acquire(WAKELOCK_TIMEOUT_MILLIS); } }); @@ -608,4 +651,10 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } return true; } + + private boolean isRegistered() { + synchronized (mRegistrationLock) { + return mIsRegistered; + } + } } diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java index ed98bf98f7b7..06aeb62a28b8 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java @@ -206,12 +206,6 @@ import java.util.function.Consumer; EndpointInfo halEndpointInfo = ContextHubServiceUtil.createHalEndpointInfo( pendingEndpointInfo, endpointId, SERVICE_HUB_ID); - try { - mHubInterface.registerEndpoint(halEndpointInfo); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException while calling HAL registerEndpoint", e); - throw e; - } broker = new ContextHubEndpointBroker( mContext, @@ -222,6 +216,7 @@ import java.util.function.Consumer; packageName, attributionTag, mTransactionManager); + broker.register(); mEndpointMap.put(endpointId, broker); try { @@ -282,6 +277,14 @@ import java.util.function.Consumer; mEndpointMap.remove(endpointId); } + /** Invoked by the service when the Context Hub HAL restarts. */ + /* package */ void onHalRestart() { + for (ContextHubEndpointBroker broker : mEndpointMap.values()) { + // The broker will close existing sessions and re-register itself + broker.onHalRestart(); + } + } + @Override public void onEndpointSessionOpenRequest( int sessionId, diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java index 2b0ca145372b..502a7aeba258 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java @@ -259,6 +259,9 @@ public class ContextHubService extends IContextHubService.Stub { if (mHubInfoRegistry != null) { mHubInfoRegistry.onHalRestart(); } + if (mEndpointManager != null) { + mEndpointManager.onHalRestart(); + } resetSettings(); if (Flags.reconnectHostEndpointsAfterHalRestart()) { mClientManager.forEachClientOfHub(mContextHubId, diff --git a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java index 12495bb4f2cc..d7d0eb40af70 100644 --- a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java +++ b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java @@ -612,25 +612,23 @@ class GnssNetworkConnectivityHandler { networkRequestBuilder.addCapability(getNetworkCapability(mAGpsType)); networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR); - if (com.android.internal.telephony.flags.Flags.satelliteInternet()) { - // Add transport type NetworkCapabilities.TRANSPORT_SATELLITE on satellite network. - TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class); - if (telephonyManager != null) { - ServiceState state = telephonyManager.getServiceState(); - if (state != null && state.isUsingNonTerrestrialNetwork()) { - networkRequestBuilder.removeCapability(NET_CAPABILITY_NOT_RESTRICTED); - try { - networkRequestBuilder.addTransportType(NetworkCapabilities - .TRANSPORT_SATELLITE); - networkRequestBuilder.removeCapability(NetworkCapabilities - .NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED); - } catch (IllegalArgumentException ignored) { - // In case TRANSPORT_SATELLITE or NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED - // are not recognized, meaning an old connectivity module runs on new - // android in which case no network with such capabilities will be brought - // up, so it's safe to ignore the exception. - // TODO: Can remove the try-catch in next quarter release. - } + // Add transport type NetworkCapabilities.TRANSPORT_SATELLITE on satellite network. + TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class); + if (telephonyManager != null) { + ServiceState state = telephonyManager.getServiceState(); + if (state != null && state.isUsingNonTerrestrialNetwork()) { + networkRequestBuilder.removeCapability(NET_CAPABILITY_NOT_RESTRICTED); + try { + networkRequestBuilder.addTransportType(NetworkCapabilities + .TRANSPORT_SATELLITE); + networkRequestBuilder.removeCapability(NetworkCapabilities + .NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED); + } catch (IllegalArgumentException ignored) { + // In case TRANSPORT_SATELLITE or NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED + // are not recognized, meaning an old connectivity module runs on new + // android in which case no network with such capabilities will be brought + // up, so it's safe to ignore the exception. + // TODO: Can remove the try-catch in next quarter release. } } } diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java index b529853c63a4..058bbc08a9ef 100644 --- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java +++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java @@ -267,6 +267,50 @@ import java.util.stream.Stream; notifyRequestFailed(requestId, MediaRoute2ProviderService.REASON_ROUTE_NOT_AVAILABLE); } + @Override + public void selectRoute(long requestId, String sessionId, String routeId) { + if (SYSTEM_SESSION_ID.equals(sessionId)) { + super.selectRoute(requestId, sessionId, routeId); + return; + } + synchronized (mLock) { + var sessionRecord = getSessionRecordByOriginalId(sessionId); + var proxyRecord = sessionRecord != null ? sessionRecord.getProxyRecord() : null; + if (proxyRecord != null) { + var targetSourceRouteId = + proxyRecord.mNewOriginalIdToSourceOriginalIdMap.get(routeId); + if (targetSourceRouteId != null) { + proxyRecord.mProxy.selectRoute( + requestId, sessionRecord.getServiceSessionId(), targetSourceRouteId); + } + return; + } + } + notifyRequestFailed(requestId, MediaRoute2ProviderService.REASON_ROUTE_NOT_AVAILABLE); + } + + @Override + public void deselectRoute(long requestId, String sessionId, String routeId) { + if (SYSTEM_SESSION_ID.equals(sessionId)) { + super.selectRoute(requestId, sessionId, routeId); + return; + } + synchronized (mLock) { + var sessionRecord = getSessionRecordByOriginalId(sessionId); + var proxyRecord = sessionRecord != null ? sessionRecord.getProxyRecord() : null; + if (proxyRecord != null) { + var targetSourceRouteId = + proxyRecord.mNewOriginalIdToSourceOriginalIdMap.get(routeId); + if (targetSourceRouteId != null) { + proxyRecord.mProxy.deselectRoute( + requestId, sessionRecord.getServiceSessionId(), targetSourceRouteId); + } + return; + } + } + notifyRequestFailed(requestId, MediaRoute2ProviderService.REASON_ROUTE_NOT_AVAILABLE); + } + @GuardedBy("mLock") private SystemMediaSessionRecord getSessionRecordByOriginalId(String sessionOriginalId) { if (FORCE_GLOBAL_ROUTING_SESSION) { diff --git a/services/core/java/com/android/server/notification/NotificationChannelExtractor.java b/services/core/java/com/android/server/notification/NotificationChannelExtractor.java index e2889fa9cbf6..18bccd8411d7 100644 --- a/services/core/java/com/android/server/notification/NotificationChannelExtractor.java +++ b/services/core/java/com/android/server/notification/NotificationChannelExtractor.java @@ -91,7 +91,7 @@ public class NotificationChannelExtractor implements NotificationSignalExtractor updateAttributes = true; } if (restrictAudioAttributesAlarm() - && record.getNotification().category != CATEGORY_ALARM + && !CATEGORY_ALARM.equals(record.getNotification().category) && attributes.getUsage() == AudioAttributes.USAGE_ALARM) { updateAttributes = true; } diff --git a/services/core/java/com/android/server/pm/AppsFilterImpl.java b/services/core/java/com/android/server/pm/AppsFilterImpl.java index cc4c2b5bf893..068d68d25017 100644 --- a/services/core/java/com/android/server/pm/AppsFilterImpl.java +++ b/services/core/java/com/android/server/pm/AppsFilterImpl.java @@ -40,6 +40,7 @@ import static com.android.server.pm.AppsFilterUtils.canQueryViaUsesLibrary; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; +import android.app.ApplicationPackageManager; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.SigningDetails; @@ -173,6 +174,10 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, * Report a change to observers. */ private void onChanged() { + // App visibility may have changed, which means that earlier fetches from these caches may + // be invalid. + PackageManager.invalidatePackageInfoCache(); + ApplicationPackageManager.invalidateGetPackagesForUidCache(); dispatchChange(this); } diff --git a/services/core/java/com/android/server/power/ThermalManagerService.java b/services/core/java/com/android/server/power/ThermalManagerService.java index f46fa446a0ba..5ee9b7d09fdd 100644 --- a/services/core/java/com/android/server/power/ThermalManagerService.java +++ b/services/core/java/com/android/server/power/ThermalManagerService.java @@ -2096,11 +2096,16 @@ public class ThermalManagerService extends SystemService { } if (mCachedHeadrooms.contains(forecastSeconds)) { - // TODO(b/360486877): replace with metrics - Slog.d(TAG, - "Headroom forecast in " + forecastSeconds + "s served from cache: " - + mCachedHeadrooms.get(forecastSeconds)); - return mCachedHeadrooms.get(forecastSeconds); + float headroom = mCachedHeadrooms.get(forecastSeconds); + // TODO(b/360486877): add new API status enum or a new atom field to + // differentiate success from reading cache or not + FrameworkStatsLog.write(FrameworkStatsLog.THERMAL_HEADROOM_CALLED, + Binder.getCallingUid(), + FrameworkStatsLog.THERMAL_HEADROOM_CALLED__API_STATUS__SUCCESS, + headroom, forecastSeconds); + Slog.d(TAG, "Headroom forecast in " + forecastSeconds + "s served from cache: " + + headroom); + return headroom; } float maxNormalized = Float.NaN; @@ -2121,9 +2126,15 @@ public class ThermalManagerService extends SystemService { if (samples.size() < MINIMUM_SAMPLE_COUNT) { if (mSamples.size() == 1 && mCachedHeadrooms.contains(0)) { // if only one sensor name exists, then try reading the cache - // TODO(b/360486877): replace with metrics - Slog.d(TAG, "Headroom forecast cached: " + mCachedHeadrooms.get(0)); - return mCachedHeadrooms.get(0); + // TODO(b/360486877): add new API status enum or a new atom field to + // differentiate success from reading cache or not + float headroom = mCachedHeadrooms.get(0); + FrameworkStatsLog.write(FrameworkStatsLog.THERMAL_HEADROOM_CALLED, + Binder.getCallingUid(), + FrameworkStatsLog.THERMAL_HEADROOM_CALLED__API_STATUS__SUCCESS, + headroom, 0); + Slog.d(TAG, "Headroom forecast in 0s served from cache: " + headroom); + return headroom; } // Don't try to forecast, just use the latest one we have float normalized = normalizeTemperature(currentTemperature, threshold); diff --git a/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java b/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java index dc1f93664f79..f060e4d11e82 100644 --- a/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java +++ b/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java @@ -17,8 +17,8 @@ package com.android.server.security; import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_BOOT_STATE; -import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_KEYSTORE_REQUIREMENTS; import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_CERTS; +import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_KEYSTORE_REQUIREMENTS; import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_LOCAL_BINDING_REQUIREMENTS; import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_PATCH_LEVEL_DIFF; import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_UNKNOWN; @@ -47,12 +47,8 @@ import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.server.security.AttestationVerificationManagerService.DumpLogger; -import org.json.JSONObject; - import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; -import java.net.URL; import java.security.InvalidAlgorithmParameterException; import java.security.cert.CertPath; import java.security.cert.CertPathValidator; @@ -60,7 +56,6 @@ import java.security.cert.CertPathValidatorException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; -import java.security.cert.PKIXCertPathChecker; import java.security.cert.PKIXParameters; import java.security.cert.TrustAnchor; import java.security.cert.X509Certificate; @@ -69,7 +64,6 @@ import java.time.ZoneId; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -126,6 +120,7 @@ class AttestationVerificationPeerDeviceVerifier { private final LocalDate mTestLocalPatchDate; private final CertificateFactory mCertificateFactory; private final CertPathValidator mCertPathValidator; + private final CertificateRevocationStatusManager mCertificateRevocationStatusManager; private final DumpLogger mDumpLogger; AttestationVerificationPeerDeviceVerifier(@NonNull Context context, @@ -135,6 +130,7 @@ class AttestationVerificationPeerDeviceVerifier { mCertificateFactory = CertificateFactory.getInstance("X.509"); mCertPathValidator = CertPathValidator.getInstance("PKIX"); mTrustAnchors = getTrustAnchors(); + mCertificateRevocationStatusManager = new CertificateRevocationStatusManager(mContext); mRevocationEnabled = true; mTestSystemDate = null; mTestLocalPatchDate = null; @@ -150,6 +146,7 @@ class AttestationVerificationPeerDeviceVerifier { mCertificateFactory = CertificateFactory.getInstance("X.509"); mCertPathValidator = CertPathValidator.getInstance("PKIX"); mTrustAnchors = trustAnchors; + mCertificateRevocationStatusManager = new CertificateRevocationStatusManager(mContext); mRevocationEnabled = revocationEnabled; mTestSystemDate = systemDate; mTestLocalPatchDate = localPatchDate; @@ -300,15 +297,14 @@ class AttestationVerificationPeerDeviceVerifier { CertPath certificatePath = mCertificateFactory.generateCertPath(certificates); PKIXParameters validationParams = new PKIXParameters(mTrustAnchors); + // Do not use built-in revocation status checker. + validationParams.setRevocationEnabled(false); + mCertPathValidator.validate(certificatePath, validationParams); if (mRevocationEnabled) { // Checks Revocation Status List based on // https://developer.android.com/training/articles/security-key-attestation#certificate_status - PKIXCertPathChecker checker = new AndroidRevocationStatusListChecker(); - validationParams.addCertPathChecker(checker); + mCertificateRevocationStatusManager.checkRevocationStatus(certificates); } - // Do not use built-in revocation status checker. - validationParams.setRevocationEnabled(false); - mCertPathValidator.validate(certificatePath, validationParams); } private Set<TrustAnchor> getTrustAnchors() throws CertPathValidatorException { @@ -574,96 +570,6 @@ class AttestationVerificationPeerDeviceVerifier { <= maxPatchLevelDiffMonths; } - /** - * Checks certificate revocation status. - * - * Queries status list from android.googleapis.com/attestation/status and checks for - * the existence of certificate's serial number. If serial number exists in map, then fail. - */ - private final class AndroidRevocationStatusListChecker extends PKIXCertPathChecker { - private static final String TOP_LEVEL_JSON_PROPERTY_KEY = "entries"; - private static final String STATUS_PROPERTY_KEY = "status"; - private static final String REASON_PROPERTY_KEY = "reason"; - private String mStatusUrl; - private JSONObject mJsonStatusMap; - - @Override - public void init(boolean forward) throws CertPathValidatorException { - mStatusUrl = getRevocationListUrl(); - if (mStatusUrl == null || mStatusUrl.isEmpty()) { - throw new CertPathValidatorException( - "R.string.vendor_required_attestation_revocation_list_url is empty."); - } - // TODO(b/221067843): Update to only pull status map on non critical path and if - // out of date (24hrs). - mJsonStatusMap = getStatusMap(mStatusUrl); - } - - @Override - public boolean isForwardCheckingSupported() { - return false; - } - - @Override - public Set<String> getSupportedExtensions() { - return null; - } - - @Override - public void check(Certificate cert, Collection<String> unresolvedCritExts) - throws CertPathValidatorException { - X509Certificate x509Certificate = (X509Certificate) cert; - // The json key is the certificate's serial number converted to lowercase hex. - String serialNumber = x509Certificate.getSerialNumber().toString(16); - - if (serialNumber == null) { - throw new CertPathValidatorException("Certificate serial number can not be null."); - } - - if (mJsonStatusMap.has(serialNumber)) { - JSONObject revocationStatus; - String status; - String reason; - try { - revocationStatus = mJsonStatusMap.getJSONObject(serialNumber); - status = revocationStatus.getString(STATUS_PROPERTY_KEY); - reason = revocationStatus.getString(REASON_PROPERTY_KEY); - } catch (Throwable t) { - throw new CertPathValidatorException("Unable get properties for certificate " - + "with serial number " + serialNumber); - } - throw new CertPathValidatorException( - "Invalid certificate with serial number " + serialNumber - + " has status " + status - + " because reason " + reason); - } - } - - private JSONObject getStatusMap(String stringUrl) throws CertPathValidatorException { - URL url; - try { - url = new URL(stringUrl); - } catch (Throwable t) { - throw new CertPathValidatorException( - "Unable to get revocation status from " + mStatusUrl, t); - } - - try (InputStream inputStream = url.openStream()) { - JSONObject statusListJson = new JSONObject( - new String(inputStream.readAllBytes(), UTF_8)); - return statusListJson.getJSONObject(TOP_LEVEL_JSON_PROPERTY_KEY); - } catch (Throwable t) { - throw new CertPathValidatorException( - "Unable to parse revocation status from " + mStatusUrl, t); - } - } - - private String getRevocationListUrl() { - return mContext.getResources().getString( - R.string.vendor_required_attestation_revocation_list_url); - } - } - /* Mutable data class for tracking dump data from verifications. */ private static class MyDumpData extends AttestationVerificationManagerService.DumpData { diff --git a/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java b/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java new file mode 100644 index 000000000000..d36d9f5f6636 --- /dev/null +++ b/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java @@ -0,0 +1,366 @@ +/* + * 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.server.security; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import android.app.job.JobInfo; +import android.app.job.JobScheduler; +import android.content.ComponentName; +import android.content.Context; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.os.Environment; +import android.os.PersistableBundle; +import android.util.Slog; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.cert.CertPathValidatorException; +import java.security.cert.X509Certificate; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** Manages the revocation status of certificates used in remote attestation. */ +class CertificateRevocationStatusManager { + private static final String TAG = "AVF_CRL"; + // Must be unique within system server + private static final int JOB_ID = 1737671340; + private static final String REVOCATION_STATUS_FILE_NAME = "certificate_revocation_status.txt"; + private static final String REVOCATION_STATUS_FILE_FIELD_DELIMITER = ","; + + /** + * The number of days since last update for which a stored revocation status can be accepted. + */ + @VisibleForTesting static final int MAX_DAYS_SINCE_LAST_CHECK = 30; + + /** + * The number of days since issue date for an intermediary certificate to be considered fresh + * and not require a revocation list check. + */ + private static final int FRESH_INTERMEDIARY_CERT_DAYS = 70; + + /** + * The expected number of days between a certificate's issue date and notBefore date. Used to + * infer a certificate's issue date from its notBefore date. + */ + private static final int DAYS_BETWEEN_ISSUE_AND_NOT_BEFORE_DATES = 2; + + private static final String TOP_LEVEL_JSON_PROPERTY_KEY = "entries"; + private static final Object sFileLock = new Object(); + + private final Context mContext; + private final String mTestRemoteRevocationListUrl; + private final File mTestRevocationStatusFile; + private final boolean mShouldScheduleJob; + + CertificateRevocationStatusManager(Context context) { + this(context, null, null, true); + } + + @VisibleForTesting + CertificateRevocationStatusManager( + Context context, + String testRemoteRevocationListUrl, + File testRevocationStatusFile, + boolean shouldScheduleJob) { + mContext = context; + mTestRemoteRevocationListUrl = testRemoteRevocationListUrl; + mTestRevocationStatusFile = testRevocationStatusFile; + mShouldScheduleJob = shouldScheduleJob; + } + + /** + * Check the revocation status of the provided {@link X509Certificate}s. + * + * <p>The provided certificates should have been validated and ordered from leaf to a + * certificate issued by the trust anchor, per the convention specified in the javadoc of {@link + * java.security.cert.CertPath}. + * + * @param certificates List of certificates to be checked + * @throws CertPathValidatorException if the check failed + */ + void checkRevocationStatus(List<X509Certificate> certificates) + throws CertPathValidatorException { + if (!needToCheckRevocationStatus(certificates)) { + return; + } + List<String> serialNumbers = new ArrayList<>(); + for (X509Certificate certificate : certificates) { + String serialNumber = certificate.getSerialNumber().toString(16); + if (serialNumber == null) { + throw new CertPathValidatorException("Certificate serial number cannot be null."); + } + serialNumbers.add(serialNumber); + } + try { + JSONObject revocationList = fetchRemoteRevocationList(); + Map<String, Boolean> areCertificatesRevoked = new HashMap<>(); + for (String serialNumber : serialNumbers) { + areCertificatesRevoked.put(serialNumber, revocationList.has(serialNumber)); + } + updateLastRevocationCheckData(areCertificatesRevoked); + for (Map.Entry<String, Boolean> entry : areCertificatesRevoked.entrySet()) { + if (entry.getValue()) { + throw new CertPathValidatorException( + "Certificate " + entry.getKey() + " has been revoked."); + } + } + } catch (IOException | JSONException ex) { + Slog.d(TAG, "Fallback to check stored revocation status", ex); + if (ex instanceof IOException && mShouldScheduleJob) { + scheduleJobToUpdateStoredDataWithRemoteRevocationList(serialNumbers); + } + for (X509Certificate certificate : certificates) { + // Assume recently issued certificates are not revoked. + if (isIssuedWithinDays(certificate, MAX_DAYS_SINCE_LAST_CHECK)) { + String serialNumber = certificate.getSerialNumber().toString(16); + serialNumbers.remove(serialNumber); + } + } + Map<String, LocalDateTime> lastRevocationCheckData; + try { + lastRevocationCheckData = getLastRevocationCheckData(); + } catch (IOException ex2) { + throw new CertPathValidatorException( + "Unable to load stored revocation status", ex2); + } + for (String serialNumber : serialNumbers) { + if (!lastRevocationCheckData.containsKey(serialNumber) + || lastRevocationCheckData + .get(serialNumber) + .isBefore( + LocalDateTime.now().minusDays(MAX_DAYS_SINCE_LAST_CHECK))) { + throw new CertPathValidatorException( + "Unable to verify the revocation status of certificate " + + serialNumber); + } + } + } + } + + private static boolean needToCheckRevocationStatus( + List<X509Certificate> certificatesOrderedLeafFirst) { + if (certificatesOrderedLeafFirst.isEmpty()) { + return false; + } + // A certificate isn't revoked when it is first issued, so we treat it as checked on its + // issue date. + if (!isIssuedWithinDays(certificatesOrderedLeafFirst.get(0), MAX_DAYS_SINCE_LAST_CHECK)) { + return true; + } + for (int i = 1; i < certificatesOrderedLeafFirst.size(); i++) { + if (!isIssuedWithinDays( + certificatesOrderedLeafFirst.get(i), FRESH_INTERMEDIARY_CERT_DAYS)) { + return true; + } + } + return false; + } + + private static boolean isIssuedWithinDays(X509Certificate certificate, int days) { + LocalDate notBeforeDate = + LocalDate.ofInstant(certificate.getNotBefore().toInstant(), ZoneId.systemDefault()); + LocalDate expectedIssueData = + notBeforeDate.plusDays(DAYS_BETWEEN_ISSUE_AND_NOT_BEFORE_DATES); + return LocalDate.now().minusDays(days + 1).isBefore(expectedIssueData); + } + + void updateLastRevocationCheckDataForAllPreviouslySeenCertificates( + JSONObject revocationList, Collection<String> otherCertificatesToCheck) { + Set<String> allCertificatesToCheck = new HashSet<>(otherCertificatesToCheck); + try { + allCertificatesToCheck.addAll(getLastRevocationCheckData().keySet()); + } catch (IOException ex) { + Slog.e(TAG, "Unable to update last check date of stored data.", ex); + } + Map<String, Boolean> areCertificatesRevoked = new HashMap<>(); + for (String serialNumber : allCertificatesToCheck) { + areCertificatesRevoked.put(serialNumber, revocationList.has(serialNumber)); + } + updateLastRevocationCheckData(areCertificatesRevoked); + } + + /** + * Update the last revocation check data stored on this device. + * + * @param areCertificatesRevoked A Map whose keys are certificate serial numbers and values are + * whether that certificate has been revoked + */ + void updateLastRevocationCheckData(Map<String, Boolean> areCertificatesRevoked) { + LocalDateTime now = LocalDateTime.now(); + synchronized (sFileLock) { + Map<String, LocalDateTime> lastRevocationCheckData; + try { + lastRevocationCheckData = getLastRevocationCheckData(); + } catch (IOException ex) { + Slog.e(TAG, "Unable to updateLastRevocationCheckData", ex); + return; + } + for (Map.Entry<String, Boolean> entry : areCertificatesRevoked.entrySet()) { + if (entry.getValue()) { + lastRevocationCheckData.remove(entry.getKey()); + } else { + lastRevocationCheckData.put(entry.getKey(), now); + } + } + storeLastRevocationCheckData(lastRevocationCheckData); + } + } + + Map<String, LocalDateTime> getLastRevocationCheckData() throws IOException { + Map<String, LocalDateTime> data = new HashMap<>(); + File dataFile = getLastRevocationCheckDataFile(); + synchronized (sFileLock) { + if (!dataFile.exists()) { + return data; + } + String dataString; + try (FileInputStream in = new FileInputStream(dataFile)) { + dataString = new String(in.readAllBytes(), UTF_8); + } + for (String line : dataString.split(System.lineSeparator())) { + String[] elements = line.split(REVOCATION_STATUS_FILE_FIELD_DELIMITER); + if (elements.length != 2) { + continue; + } + try { + data.put(elements[0], LocalDateTime.parse(elements[1])); + } catch (DateTimeParseException ex) { + Slog.e( + TAG, + "Unable to parse last checked LocalDateTime from file. Deleting the" + + " potentially corrupted file.", + ex); + dataFile.delete(); + return data; + } + } + } + return data; + } + + @VisibleForTesting + void storeLastRevocationCheckData(Map<String, LocalDateTime> lastRevocationCheckData) { + StringBuilder dataStringBuilder = new StringBuilder(); + for (Map.Entry<String, LocalDateTime> entry : lastRevocationCheckData.entrySet()) { + dataStringBuilder + .append(entry.getKey()) + .append(REVOCATION_STATUS_FILE_FIELD_DELIMITER) + .append(entry.getValue()) + .append(System.lineSeparator()); + } + synchronized (sFileLock) { + try (FileOutputStream fileOutputStream = + new FileOutputStream(getLastRevocationCheckDataFile())) { + fileOutputStream.write(dataStringBuilder.toString().getBytes(UTF_8)); + Slog.d(TAG, "Successfully stored revocation status data."); + } catch (IOException ex) { + Slog.e(TAG, "Failed to store revocation status data.", ex); + } + } + } + + private File getLastRevocationCheckDataFile() { + if (mTestRevocationStatusFile != null) { + return mTestRevocationStatusFile; + } + return new File(Environment.getDataSystemDirectory(), REVOCATION_STATUS_FILE_NAME); + } + + private void scheduleJobToUpdateStoredDataWithRemoteRevocationList(List<String> serialNumbers) { + JobScheduler jobScheduler = mContext.getSystemService(JobScheduler.class); + if (jobScheduler == null) { + Slog.e(TAG, "Unable to get job scheduler."); + return; + } + Slog.d(TAG, "Scheduling job to fetch remote CRL."); + PersistableBundle extras = new PersistableBundle(); + extras.putStringArray( + UpdateCertificateRevocationStatusJobService.EXTRA_KEY_CERTIFICATES_TO_CHECK, + serialNumbers.toArray(new String[0])); + jobScheduler.schedule( + new JobInfo.Builder( + JOB_ID, + new ComponentName( + mContext, + UpdateCertificateRevocationStatusJobService.class)) + .setExtras(extras) + .setRequiredNetwork( + new NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build()) + .build()); + } + + /** + * Fetches the revocation list from the URL specified in + * R.string.vendor_required_attestation_revocation_list_url + * + * @return The remote revocation list entries in a JSONObject + * @throws CertPathValidatorException if the URL is not defined or is malformed. + * @throws IOException if the URL is valid but the fetch failed. + * @throws JSONException if the revocation list content cannot be parsed + */ + JSONObject fetchRemoteRevocationList() + throws CertPathValidatorException, IOException, JSONException { + String urlString = getRemoteRevocationListUrl(); + if (urlString == null || urlString.isEmpty()) { + throw new CertPathValidatorException( + "R.string.vendor_required_attestation_revocation_list_url is empty."); + } + URL url; + try { + url = new URL(urlString); + } catch (MalformedURLException ex) { + throw new CertPathValidatorException("Unable to parse the URL " + urlString, ex); + } + byte[] revocationListBytes; + try (InputStream inputStream = url.openStream()) { + revocationListBytes = inputStream.readAllBytes(); + } + JSONObject revocationListJson = new JSONObject(new String(revocationListBytes, UTF_8)); + return revocationListJson.getJSONObject(TOP_LEVEL_JSON_PROPERTY_KEY); + } + + private String getRemoteRevocationListUrl() { + if (mTestRemoteRevocationListUrl != null) { + return mTestRemoteRevocationListUrl; + } + return mContext.getResources() + .getString(R.string.vendor_required_attestation_revocation_list_url); + } +} diff --git a/services/core/java/com/android/server/security/OWNERS b/services/core/java/com/android/server/security/OWNERS index fa4bf228c683..7a31a0006bb9 100644 --- a/services/core/java/com/android/server/security/OWNERS +++ b/services/core/java/com/android/server/security/OWNERS @@ -3,5 +3,6 @@ include /core/java/android/security/OWNERS per-file *AttestationVerification* = file:/core/java/android/security/attestationverification/OWNERS +per-file *CertificateRevocationStatus* = file:/core/java/android/security/attestationverification/OWNERS per-file FileIntegrity*.java = victorhsieh@google.com per-file KeyChainSystemService.java = file:platform/packages/apps/KeyChain:/OWNERS diff --git a/services/core/java/com/android/server/security/UpdateCertificateRevocationStatusJobService.java b/services/core/java/com/android/server/security/UpdateCertificateRevocationStatusJobService.java new file mode 100644 index 000000000000..768c812f47a3 --- /dev/null +++ b/services/core/java/com/android/server/security/UpdateCertificateRevocationStatusJobService.java @@ -0,0 +1,81 @@ +/* + * 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.server.security; + +import android.app.job.JobParameters; +import android.app.job.JobService; +import android.util.Slog; + +import org.json.JSONObject; + +import java.util.Arrays; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** A {@link JobService} that fetches the certificate revocation list from a remote location. */ +public class UpdateCertificateRevocationStatusJobService extends JobService { + + static final String EXTRA_KEY_CERTIFICATES_TO_CHECK = + "com.android.server.security.extra.CERTIFICATES_TO_CHECK"; + private static final String TAG = "AVF_CRL"; + private ExecutorService mExecutorService; + + @Override + public void onCreate() { + super.onCreate(); + mExecutorService = Executors.newSingleThreadExecutor(); + } + + @Override + public boolean onStartJob(JobParameters params) { + mExecutorService.execute( + () -> { + try { + CertificateRevocationStatusManager certificateRevocationStatusManager = + new CertificateRevocationStatusManager(this); + Slog.d(TAG, "Starting to fetch remote CRL from job service."); + JSONObject revocationList = + certificateRevocationStatusManager.fetchRemoteRevocationList(); + String[] certificatesToCheckFromJobParams = + params.getExtras().getStringArray(EXTRA_KEY_CERTIFICATES_TO_CHECK); + if (certificatesToCheckFromJobParams == null) { + Slog.e(TAG, "Extras not found: " + EXTRA_KEY_CERTIFICATES_TO_CHECK); + return; + } + certificateRevocationStatusManager + .updateLastRevocationCheckDataForAllPreviouslySeenCertificates( + revocationList, + Arrays.asList(certificatesToCheckFromJobParams)); + } catch (Throwable t) { + Slog.e(TAG, "Unable to update the stored revocation status.", t); + } + jobFinished(params, false); + }); + return true; + } + + @Override + public boolean onStopJob(JobParameters params) { + return false; + } + + @Override + public void onDestroy() { + super.onDestroy(); + mExecutorService.shutdown(); + } +} diff --git a/services/core/java/com/android/server/storage/ImmutableVolumeInfo.java b/services/core/java/com/android/server/storage/ImmutableVolumeInfo.java new file mode 100644 index 000000000000..9d60a576d9bc --- /dev/null +++ b/services/core/java/com/android/server/storage/ImmutableVolumeInfo.java @@ -0,0 +1,139 @@ +/* + * 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.server.storage; + +import android.content.Context; +import android.os.storage.DiskInfo; +import android.os.storage.StorageVolume; +import android.os.storage.VolumeInfo; + +import com.android.internal.util.IndentingPrintWriter; + +import java.io.File; + +/** + * An immutable version of {@link VolumeInfo} with only getters. + * + * @hide + */ +public final class ImmutableVolumeInfo { + private final VolumeInfo mVolumeInfo; + + private ImmutableVolumeInfo(VolumeInfo volumeInfo) { + mVolumeInfo = new VolumeInfo(volumeInfo); + } + + public static ImmutableVolumeInfo fromVolumeInfo(VolumeInfo info) { + return new ImmutableVolumeInfo(info); + } + + public ImmutableVolumeInfo clone() { + return fromVolumeInfo(mVolumeInfo.clone()); + } + + public StorageVolume buildStorageVolume(Context context, int userId, boolean reportUnmounted) { + return mVolumeInfo.buildStorageVolume(context, userId, reportUnmounted); + } + + public void dump(IndentingPrintWriter pw) { + mVolumeInfo.dump(pw); + } + + public DiskInfo getDisk() { + return mVolumeInfo.getDisk(); + } + + public String getDiskId() { + return mVolumeInfo.getDiskId(); + } + + public String getFsLabel() { + return mVolumeInfo.fsLabel; + } + + public String getFsPath() { + return mVolumeInfo.path; + } + + public String getFsType() { + return mVolumeInfo.fsType; + } + + public String getFsUuid() { + return mVolumeInfo.fsUuid; + } + + public String getId() { + return mVolumeInfo.id; + } + + public File getInternalPath() { + return mVolumeInfo.getInternalPath(); + } + + public int getMountFlags() { + return mVolumeInfo.mountFlags; + } + + public int getMountUserId() { + return mVolumeInfo.mountUserId; + } + + public String getPartGuid() { + return mVolumeInfo.partGuid; + } + + public File getPath() { + return mVolumeInfo.getPath(); + } + + public int getState() { + return mVolumeInfo.state; + } + + public int getType() { + return mVolumeInfo.type; + } + + public VolumeInfo getVolumeInfo() { + return new VolumeInfo(mVolumeInfo); // Return a copy, not the original + } + + public boolean isMountedReadable() { + return mVolumeInfo.isMountedReadable(); + } + + public boolean isMountedWritable() { + return mVolumeInfo.isMountedWritable(); + } + + public boolean isPrimary() { + return mVolumeInfo.isPrimary(); + } + + public boolean isVisible() { + return mVolumeInfo.isVisible(); + } + + public boolean isVisibleForUser(int userId) { + return mVolumeInfo.isVisibleForUser(userId); + } + + public boolean isVisibleForWrite(int userId) { + return mVolumeInfo.isVisibleForWrite(userId); + } +} diff --git a/services/core/java/com/android/server/storage/StorageSessionController.java b/services/core/java/com/android/server/storage/StorageSessionController.java index b9c9b64cd2c6..342b864c6473 100644 --- a/services/core/java/com/android/server/storage/StorageSessionController.java +++ b/services/core/java/com/android/server/storage/StorageSessionController.java @@ -45,6 +45,7 @@ import android.util.Slog; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; +import com.android.server.storage.ImmutableVolumeInfo; import java.util.Objects; @@ -79,18 +80,18 @@ public final class StorageSessionController { * @param vol for which the storage session has to be started * @return userId for connection for this volume */ - public int getConnectionUserIdForVolume(VolumeInfo vol) { + public int getConnectionUserIdForVolume(ImmutableVolumeInfo vol) { final Context volumeUserContext = mContext.createContextAsUser( - UserHandle.of(vol.mountUserId), 0); + UserHandle.of(vol.getMountUserId()), 0); boolean isMediaSharedWithParent = volumeUserContext.getSystemService( UserManager.class).isMediaSharedWithParent(); - UserInfo userInfo = mUserManager.getUserInfo(vol.mountUserId); + UserInfo userInfo = mUserManager.getUserInfo(vol.getMountUserId()); if (userInfo != null && isMediaSharedWithParent) { // Clones use the same connection as their parent return userInfo.profileGroupId; } else { - return vol.mountUserId; + return vol.getMountUserId(); } } @@ -108,7 +109,7 @@ public final class StorageSessionController { * @throws ExternalStorageServiceException if the session fails to start * @throws IllegalStateException if a session has already been created for {@code vol} */ - public void onVolumeMount(ParcelFileDescriptor deviceFd, VolumeInfo vol) + public void onVolumeMount(ParcelFileDescriptor deviceFd, ImmutableVolumeInfo vol) throws ExternalStorageServiceException { if (!shouldHandle(vol)) { return; @@ -144,7 +145,8 @@ public final class StorageSessionController { * * @throws ExternalStorageServiceException if it fails to connect to ExternalStorageService */ - public void notifyVolumeStateChanged(VolumeInfo vol) throws ExternalStorageServiceException { + public void notifyVolumeStateChanged(ImmutableVolumeInfo vol) + throws ExternalStorageServiceException { if (!shouldHandle(vol)) { return; } @@ -214,7 +216,7 @@ public final class StorageSessionController { * @return the connection that was removed or {@code null} if nothing was removed */ @Nullable - public StorageUserConnection onVolumeRemove(VolumeInfo vol) { + public StorageUserConnection onVolumeRemove(ImmutableVolumeInfo vol) { if (!shouldHandle(vol)) { return null; } @@ -246,7 +248,7 @@ public final class StorageSessionController { * * Call {@link #onVolumeRemove} to remove the connection without waiting for exit */ - public void onVolumeUnmount(VolumeInfo vol) { + public void onVolumeUnmount(ImmutableVolumeInfo vol) { String sessionId = vol.getId(); final long token = Binder.clearCallingIdentity(); try { @@ -457,9 +459,9 @@ public final class StorageSessionController { * Returns {@code true} if {@code vol} is an emulated or visible public volume, * {@code false} otherwise **/ - public static boolean isEmulatedOrPublic(VolumeInfo vol) { - return vol.type == VolumeInfo.TYPE_EMULATED - || (vol.type == VolumeInfo.TYPE_PUBLIC && vol.isVisible()); + public static boolean isEmulatedOrPublic(ImmutableVolumeInfo vol) { + return vol.getType() == VolumeInfo.TYPE_EMULATED + || (vol.getType() == VolumeInfo.TYPE_PUBLIC && vol.isVisible()); } /** Exception thrown when communication with the {@link ExternalStorageService} fails. */ @@ -477,11 +479,11 @@ public final class StorageSessionController { } } - private static boolean isSupportedVolume(VolumeInfo vol) { - return isEmulatedOrPublic(vol) || vol.type == VolumeInfo.TYPE_STUB; + private static boolean isSupportedVolume(ImmutableVolumeInfo vol) { + return isEmulatedOrPublic(vol) || vol.getType() == VolumeInfo.TYPE_STUB; } - private boolean shouldHandle(@Nullable VolumeInfo vol) { + private boolean shouldHandle(@Nullable ImmutableVolumeInfo vol) { return !mIsResetting && (vol == null || isSupportedVolume(vol)); } diff --git a/services/core/java/com/android/server/storage/WatchedVolumeInfo.java b/services/core/java/com/android/server/storage/WatchedVolumeInfo.java new file mode 100644 index 000000000000..4124cfb4f092 --- /dev/null +++ b/services/core/java/com/android/server/storage/WatchedVolumeInfo.java @@ -0,0 +1,206 @@ +/* + * 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.server.storage; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.os.storage.DiskInfo; +import android.os.storage.StorageVolume; +import android.os.storage.VolumeInfo; + +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.utils.Watchable; +import com.android.server.utils.WatchableImpl; + +import java.io.File; + +/** + * A wrapper for {@link VolumeInfo} implementing the {@link Watchable} interface. + * + * The {@link VolumeInfo} class itself cannot safely implement Watchable, because it has several + * UnsupportedAppUsage annotations and public fields, which allow it to be modified without + * notifying watchers. + * + * @hide + */ +public class WatchedVolumeInfo extends WatchableImpl { + private final VolumeInfo mVolumeInfo; + + private WatchedVolumeInfo(VolumeInfo volumeInfo) { + mVolumeInfo = volumeInfo; + } + + public WatchedVolumeInfo(WatchedVolumeInfo watchedVolumeInfo) { + mVolumeInfo = new VolumeInfo(watchedVolumeInfo.mVolumeInfo); + } + + public static WatchedVolumeInfo fromVolumeInfo(VolumeInfo info) { + return new WatchedVolumeInfo(info); + } + + /** + * Returns a copy of the embedded VolumeInfo object, to be used by components + * that just need it for retrieving some state from it. + * + * @return A copy of the embedded VolumeInfo object + */ + + public WatchedVolumeInfo clone() { + return fromVolumeInfo(mVolumeInfo.clone()); + } + + public ImmutableVolumeInfo getImmutableVolumeInfo() { + return ImmutableVolumeInfo.fromVolumeInfo(mVolumeInfo); + } + + public StorageVolume buildStorageVolume(Context context, int userId, boolean reportUnmounted) { + return mVolumeInfo.buildStorageVolume(context, userId, reportUnmounted); + } + + public void dump(IndentingPrintWriter pw) { + mVolumeInfo.dump(pw); + } + + public DiskInfo getDisk() { + return mVolumeInfo.getDisk(); + } + + public String getDiskId() { + return mVolumeInfo.getDiskId(); + } + + public String getFsLabel() { + return mVolumeInfo.fsLabel; + } + + public void setFsLabel(String fsLabel) { + mVolumeInfo.fsLabel = fsLabel; + dispatchChange(this); + } + + public String getFsPath() { + return mVolumeInfo.path; + } + + public void setFsPath(String path) { + mVolumeInfo.path = path; + dispatchChange(this); + } + + public String getFsType() { + return mVolumeInfo.fsType; + } + + public void setFsType(String fsType) { + mVolumeInfo.fsType = fsType; + dispatchChange(this); + } + + public @Nullable String getFsUuid() { + return mVolumeInfo.fsUuid; + } + + public void setFsUuid(String fsUuid) { + mVolumeInfo.fsUuid = fsUuid; + dispatchChange(this); + } + + public @NonNull String getId() { + return mVolumeInfo.id; + } + + public File getInternalPath() { + return mVolumeInfo.getInternalPath(); + } + + public void setInternalPath(String internalPath) { + mVolumeInfo.internalPath = internalPath; + dispatchChange(this); + } + + public int getMountFlags() { + return mVolumeInfo.mountFlags; + } + + public void setMountFlags(int mountFlags) { + mVolumeInfo.mountFlags = mountFlags; + dispatchChange(this); + } + + public int getMountUserId() { + return mVolumeInfo.mountUserId; + } + + public void setMountUserId(int mountUserId) { + mVolumeInfo.mountUserId = mountUserId; + dispatchChange(this); + } + + public String getPartGuid() { + return mVolumeInfo.partGuid; + } + + public File getPath() { + return mVolumeInfo.getPath(); + } + + public int getState() { + return mVolumeInfo.state; + } + + public int getState(int state) { + return mVolumeInfo.state; + } + + public void setState(int state) { + mVolumeInfo.state = state; + dispatchChange(this); + } + + public int getType() { + return mVolumeInfo.type; + } + + public VolumeInfo getVolumeInfo() { + return new VolumeInfo(mVolumeInfo); + } + + public boolean isMountedReadable() { + return mVolumeInfo.isMountedReadable(); + } + + public boolean isMountedWritable() { + return mVolumeInfo.isMountedWritable(); + } + + public boolean isPrimary() { + return mVolumeInfo.isPrimary(); + } + + public boolean isVisible() { + return mVolumeInfo.isVisible(); + } + + public boolean isVisibleForUser(int userId) { + return mVolumeInfo.isVisibleForUser(userId); + } + + public boolean isVisibleForWrite(int userId) { + return mVolumeInfo.isVisibleForWrite(userId); + } +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index ef63229f55e2..69b2b9b326ba 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -777,7 +777,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub mWallpaperCompatibleDisplaysForTest.remove(displayId); } - private void updateFallbackConnection() { + private void updateFallbackConnection(int clientUid) { if (mLastWallpaper == null || mFallbackWallpaper == null) return; final WallpaperConnection systemConnection = mLastWallpaper.connection; final WallpaperConnection fallbackConnection = mFallbackWallpaper.connection; @@ -793,8 +793,12 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } if (isDeviceEligibleForDesktopExperienceWallpaper(mContext)) { - mWallpaperDisplayHelper.forEachDisplayData(displayData -> { - int displayId = displayData.mDisplayId; + Display[] displays = mWallpaperDisplayHelper.getDisplays(); + for (int i = displays.length - 1; i >= 0; i--) { + int displayId = displays[i].getDisplayId(); + if (!mWallpaperDisplayHelper.isUsableDisplay(displayId, clientUid)) { + continue; + } // If the display is already connected to the desired wallpaper(s), either the // same wallpaper for both lock and system, or different wallpapers for each, // any existing fallback wallpaper connection will be removed. @@ -802,11 +806,13 @@ public class WallpaperManagerService extends IWallpaperManager.Stub && (lockConnection == null || lockConnection.containsDisplay(displayId))) { DisplayConnector fallbackConnector = mFallbackWallpaper.connection.mDisplayConnector.get(displayId); - if (fallbackConnector != null && fallbackConnector.mEngine != null) { - fallbackConnector.disconnectLocked(mFallbackWallpaper.connection); + if (fallbackConnector != null) { + if (fallbackConnector.mEngine != null) { + fallbackConnector.disconnectLocked(mFallbackWallpaper.connection); + } mFallbackWallpaper.connection.mDisplayConnector.remove(displayId); } - return; + continue; } // Identify if the fallback wallpaper should be use for lock or system or both. @@ -844,7 +850,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub mFallbackWallpaper); } } - }); + } } else if (isWallpaperCompatibleForDisplay(DEFAULT_DISPLAY, systemConnection)) { if (fallbackConnection.mDisplayConnector.size() != 0) { fallbackConnection.forEachDisplayConnector(connector -> { @@ -3787,7 +3793,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub wallpaper.connection = newConn; newConn.mReply = reply; updateCurrentWallpapers(wallpaper); - updateFallbackConnection(); + updateFallbackConnection(componentUid); } catch (RemoteException e) { String msg = "Remote exception for " + componentName + "\n" + e; if (fromUser) { diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index b17eef85f93d..d84016b3816e 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -246,9 +246,7 @@ import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_STARTING_WIND import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL; import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_WILL_PLACE_SURFACES; -import static com.android.server.wm.WindowManagerService.sEnableShellTransitions; import static com.android.server.wm.WindowState.LEGACY_POLICY_VISIBILITY; -import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays; import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; import static org.xmlpull.v1.XmlPullParser.END_TAG; @@ -764,13 +762,6 @@ final class ActivityRecord extends WindowToken { boolean mLastImeShown; /** - * When set to true, the IME insets will be frozen until the next app becomes IME input target. - * @see InsetsPolicy#adjustVisibilityForIme - * @see ImeInsetsSourceProvider#updateClientVisibility - */ - boolean mImeInsetsFrozenUntilStartInput; - - /** * A flag to determine if this AR is in the process of closing or entering PIP. This is needed * to help AR know that the app is in the process of closing but hasn't yet started closing on * the WM side. @@ -1175,8 +1166,6 @@ final class ActivityRecord extends WindowToken { pw.print(" launchMode="); pw.println(launchMode); pw.print(prefix); pw.print("mActivityType="); pw.println(activityTypeToString(getActivityType())); - pw.print(prefix); pw.print("mImeInsetsFrozenUntilStartInput="); - pw.println(mImeInsetsFrozenUntilStartInput); if (requestedVrComponent != null) { pw.print(prefix); pw.print("requestedVrComponent="); @@ -5239,7 +5228,8 @@ final class ActivityRecord extends WindowToken { pendingOptions.getWidth(), pendingOptions.getHeight()); options = AnimationOptions.makeScaleUpAnimOptions( pendingOptions.getStartX(), pendingOptions.getStartY(), - pendingOptions.getWidth(), pendingOptions.getHeight()); + pendingOptions.getWidth(), pendingOptions.getHeight(), + pendingOptions.getOverrideTaskTransition()); if (intent.getSourceBounds() == null) { intent.setSourceBounds(new Rect(pendingOptions.getStartX(), pendingOptions.getStartY(), @@ -5771,19 +5761,16 @@ final class ActivityRecord extends WindowToken { return; } - final int windowsCount = mChildren.size(); - // With Shell-Transition, the activity will running a transition when it is visible. - // It won't be included when fromTransition is true means the call from finishTransition. - final boolean runningAnimation = sEnableShellTransitions ? visible - : isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION); - for (int i = 0; i < windowsCount; i++) { - mChildren.get(i).onAppVisibilityChanged(visible, runningAnimation); + if (!visible) { + for (int i = mChildren.size() - 1; i >= 0; --i) { + mChildren.get(i).onAppCommitInvisible(); + } } setVisible(visible); setVisibleRequested(visible); ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "commitVisibility: %s: visible=%b" - + " visibleRequested=%b, isInTransition=%b, runningAnimation=%b, caller=%s", - this, isVisible(), mVisibleRequested, isInTransition(), runningAnimation, + + " visibleRequested=%b, inTransition=%b, caller=%s", + this, visible, mVisibleRequested, inTransition(), Debug.getCallers(5)); if (visible) { // If we are being set visible, and the starting window is not yet displayed, @@ -5873,10 +5860,6 @@ final class ActivityRecord extends WindowToken { } final DisplayContent displayContent = getDisplayContent(); - if (!visible) { - mImeInsetsFrozenUntilStartInput = true; - } - if (!displayContent.mClosingApps.contains(this) && !displayContent.mOpeningApps.contains(this) && !fromTransition) { @@ -6224,13 +6207,8 @@ final class ActivityRecord extends WindowToken { return false; } - // Hide all activities on the presenting display so that malicious apps can't do tap - // jacking (b/391466268). - // For now, this should only be applied to external displays because presentations can only - // be shown on them. - // TODO(b/390481621): Disallow a presentation from covering its controlling activity so that - // the presentation won't stop its controlling activity. - if (enablePresentationForConnectedDisplays() && mDisplayContent.mIsPresenting) { + // A presentation stopps all activities behind on the same display. + if (mWmService.mPresentationController.shouldOccludeActivities(getDisplayId())) { return false; } @@ -6952,14 +6930,6 @@ final class ActivityRecord extends WindowToken { // closing activity having to wait until idle timeout to be stopped or destroyed if the // next activity won't report idle (e.g. repeated view animation). mTaskSupervisor.scheduleProcessStoppingAndFinishingActivitiesIfNeeded(); - - // If the activity is visible, but no windows are eligible to start input, unfreeze - // to avoid permanently frozen IME insets. - if (mImeInsetsFrozenUntilStartInput && getWindow( - win -> WindowManager.LayoutParams.mayUseInputMethod(win.mAttrs.flags)) - == null) { - mImeInsetsFrozenUntilStartInput = false; - } } } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index ddb9f178cb8b..254127dee7a8 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -4324,10 +4324,12 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { task = mRootWindowContainer.getDefaultTaskDisplayArea().getRootTask( t -> t.isActivityTypeStandard()); } - if (task != null && task.getTopMostActivity() != null - && !task.getTopMostActivity().isState(FINISHING, DESTROYING, DESTROYED)) { + final ActivityRecord topActivity = task != null + ? task.getTopMostActivity() + : null; + if (topActivity != null && !topActivity.isState(FINISHING, DESTROYING, DESTROYED)) { mWindowManager.mAtmService.mActivityClientController - .onPictureInPictureUiStateChanged(task.getTopMostActivity(), pipState); + .onPictureInPictureUiStateChanged(topActivity, pipState); } } } diff --git a/services/core/java/com/android/server/wm/AppWarnings.java b/services/core/java/com/android/server/wm/AppWarnings.java index 576e5d5d0cd2..439b503c0c57 100644 --- a/services/core/java/com/android/server/wm/AppWarnings.java +++ b/services/core/java/com/android/server/wm/AppWarnings.java @@ -506,6 +506,10 @@ class AppWarnings { context = new ContextThemeWrapper(context, context.getThemeResId()) { @Override public void startActivity(Intent intent) { + // PageSizeMismatch dialog stays on top of the browser even after opening link + // set broadcast to close the dialog when link has been clicked. + sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); super.startActivity(intent); } diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index 79bed3d8453d..e76a83453a9d 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -388,8 +388,7 @@ class BackNavigationController { removedWindowContainer); mBackAnimationInProgress = builder != null; if (mBackAnimationInProgress) { - if (removedWindowContainer.mTransitionController.inTransition() - || mWindowManagerService.mSyncEngine.hasPendingSyncSets()) { + if (removedWindowContainer.mTransitionController.inTransition()) { ProtoLog.w(WM_DEBUG_BACK_PREVIEW, "Pending back animation due to another animation is running"); mPendingAnimationBuilder = builder; @@ -817,6 +816,8 @@ class BackNavigationController { if (openingTransition && !visible && mAnimationHandler.isTarget(ar, false /* open */) && ar.mTransitionController.isCollecting(ar)) { final TransitionController controller = ar.mTransitionController; + final Transition transition = controller.getCollectingTransition(); + final int switchType = mAnimationHandler.mOpenAnimAdaptor.mAdaptors[0].mSwitchType; boolean collectTask = false; ActivityRecord changedActivity = null; for (int i = mAnimationHandler.mOpenActivities.length - 1; i >= 0; --i) { @@ -829,8 +830,16 @@ class BackNavigationController { changedActivity = next; } } - if (collectTask && mAnimationHandler.mOpenAnimAdaptor.mAdaptors[0].mSwitchType - == AnimationHandler.TASK_SWITCH) { + if (Flags.unifyBackNavigationTransition()) { + for (int i = mAnimationHandler.mOpenAnimAdaptor.mAdaptors.length - 1; i >= 0; --i) { + collectAnimatableTarget(transition, switchType, + mAnimationHandler.mOpenAnimAdaptor.mAdaptors[i].mTarget, + false /* isTop */); + } + collectAnimatableTarget(transition, switchType, + mAnimationHandler.mCloseAdaptor.mTarget, true /* isTop */); + } + if (collectTask && switchType == AnimationHandler.TASK_SWITCH) { final Task topTask = mAnimationHandler.mOpenAnimAdaptor.mAdaptors[0].getTopTask(); if (topTask != null) { WindowContainer parent = mAnimationHandler.mOpenActivities[0].getParent(); @@ -848,6 +857,18 @@ class BackNavigationController { } } + private static void collectAnimatableTarget(Transition transition, int switchType, + WindowContainer animatingTarget, boolean isTop) { + if ((switchType == AnimationHandler.ACTIVITY_SWITCH + && (animatingTarget.asActivityRecord() != null + || animatingTarget.asTaskFragment() != null)) + || (switchType == AnimationHandler.TASK_SWITCH + && animatingTarget.asTask() != null)) { + transition.collect(animatingTarget); + transition.setBackGestureAnimation(animatingTarget, isTop); + } + } + // For shell transition /** * Check whether the transition targets was animated by back gesture animation. @@ -992,8 +1013,8 @@ class BackNavigationController { return; } - if (mWindowManagerService.mRoot.mTransitionController.isCollecting()) { - Slog.v(TAG, "Skip predictive back transition, another transition is collecting"); + if (mWindowManagerService.mRoot.mTransitionController.inTransition()) { + Slog.v(TAG, "Skip predictive back transition, another transition is playing"); cancelPendingAnimation(); return; } @@ -1098,7 +1119,7 @@ class BackNavigationController { } final Transition prepareTransition = builder.prepareTransitionIfNeeded( - openingActivities); + openingActivities, close, open); final SurfaceControl.Transaction st = openingActivities[0].getSyncTransaction(); final SurfaceControl.Transaction ct = prepareTransition != null ? st : close.getPendingTransaction(); @@ -1790,7 +1811,8 @@ class BackNavigationController { return wc == mCloseTarget || mCloseTarget.hasChild(wc) || wc.hasChild(mCloseTarget); } - private Transition prepareTransitionIfNeeded(ActivityRecord[] visibleOpenActivities) { + private Transition prepareTransitionIfNeeded(ActivityRecord[] visibleOpenActivities, + WindowContainer promoteToClose, WindowContainer[] promoteToOpen) { if (Flags.unifyBackNavigationTransition()) { if (mCloseTarget.asWindowState() != null) { return null; @@ -1806,11 +1828,11 @@ class BackNavigationController { final TransitionController tc = visibleOpenActivities[0].mTransitionController; final Transition prepareOpen = tc.createTransition( TRANSIT_PREPARE_BACK_NAVIGATION); - tc.collect(mCloseTarget); - prepareOpen.setBackGestureAnimation(mCloseTarget, true /* isTop */); - for (int i = mOpenTargets.length - 1; i >= 0; --i) { - tc.collect(mOpenTargets[i]); - prepareOpen.setBackGestureAnimation(mOpenTargets[i], false /* isTop */); + tc.collect(promoteToClose); + prepareOpen.setBackGestureAnimation(promoteToClose, true /* isTop */); + for (int i = promoteToOpen.length - 1; i >= 0; --i) { + tc.collect(promoteToOpen[i]); + prepareOpen.setBackGestureAnimation(promoteToOpen[i], false /* isTop */); } if (!makeVisibles.isEmpty()) { setLaunchBehind(visibleOpenActivities); diff --git a/services/core/java/com/android/server/wm/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java index a1faa7573a0c..f35930700653 100644 --- a/services/core/java/com/android/server/wm/DesktopModeHelper.java +++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java @@ -51,8 +51,13 @@ public final class DesktopModeHelper { } /** - * Return {@code true} if the current device supports desktop mode. + * Return {@code true} if the current device can hosts desktop sessions on its internal display. */ + @VisibleForTesting + static boolean canInternalDisplayHostDesktops(@NonNull Context context) { + return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops); + } + // TODO(b/337819319): use a companion object instead. private static boolean isDesktopModeSupported(@NonNull Context context) { return context.getResources().getBoolean(R.bool.config_isDesktopModeSupported); @@ -67,12 +72,12 @@ public final class DesktopModeHelper { */ private static boolean isDesktopModeEnabledByDevOption(@NonNull Context context) { return DesktopModeFlags.isDesktopModeForcedEnabled() && (isDesktopModeDevOptionsSupported( - context) || isDeviceEligibleForDesktopMode(context)); + context) || isInternalDisplayEligibleToHostDesktops(context)); } @VisibleForTesting - static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) { - return !shouldEnforceDeviceRestrictions() || isDesktopModeSupported(context) || ( + static boolean isInternalDisplayEligibleToHostDesktops(@NonNull Context context) { + return !shouldEnforceDeviceRestrictions() || canInternalDisplayHostDesktops(context) || ( Flags.enableDesktopModeThroughDevOption() && isDesktopModeDevOptionsSupported( context)); } @@ -81,12 +86,14 @@ public final class DesktopModeHelper { * Return {@code true} if desktop mode can be entered on the current device. */ static boolean canEnterDesktopMode(@NonNull Context context) { - return (isDesktopModeEnabled() && isDeviceEligibleForDesktopMode(context)) + return (isInternalDisplayEligibleToHostDesktops(context) + && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue() + && (isDesktopModeSupported(context) || !shouldEnforceDeviceRestrictions())) || isDesktopModeEnabledByDevOption(context); } /** Returns {@code true} if desktop experience wallpaper is supported on this device. */ public static boolean isDeviceEligibleForDesktopExperienceWallpaper(@NonNull Context context) { - return enableConnectedDisplaysWallpaper() && isDeviceEligibleForDesktopMode(context); + return enableConnectedDisplaysWallpaper() && canEnterDesktopMode(context); } } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 5cb39d44586f..c87087f84399 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -547,9 +547,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // TODO(multi-display): remove some of the usages. boolean isDefaultDisplay; - /** Indicates whether any presentation is shown on this display. */ - boolean mIsPresenting; - /** Save allocating when calculating rects */ private final Rect mTmpRect = new Rect(); private final Region mTmpRegion = new Region(); @@ -4661,35 +4658,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } } - /** - * Callback from {@link ImeInsetsSourceProvider#updateClientVisibility} for the system to - * judge whether or not to notify the IME insets provider to dispatch this reported IME client - * visibility state to the app clients when needed. - */ - boolean onImeInsetsClientVisibilityUpdate() { - boolean[] changed = new boolean[1]; - - // Unlike the IME layering target or the control target can be updated during the layout - // change, the IME input target requires to be changed after gaining the input focus. - // In case unfreezing IME insets state may too early during IME focus switching, we unfreeze - // when activities going to be visible until the input target changed, or the - // activity was the current input target that has to unfreeze after updating the IME - // client visibility. - final ActivityRecord inputTargetActivity = - mImeInputTarget != null ? mImeInputTarget.getActivityRecord() : null; - final boolean targetChanged = mImeInputTarget != mLastImeInputTarget; - if (targetChanged || inputTargetActivity != null && inputTargetActivity.isVisibleRequested() - && inputTargetActivity.mImeInsetsFrozenUntilStartInput) { - forAllActivities(r -> { - if (r.mImeInsetsFrozenUntilStartInput && r.isVisibleRequested()) { - r.mImeInsetsFrozenUntilStartInput = false; - changed[0] = true; - } - }); - } - return changed[0]; - } - void updateImeControlTarget() { updateImeControlTarget(false /* forceUpdateImeParent */); } @@ -6635,6 +6603,22 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp .getKeyguardController().isKeyguardLocked(mDisplayId); } + boolean isKeyguardLockedOrAodShowing() { + return isKeyguardLocked() || isAodShowing(); + } + + /** + * @return whether aod is showing for this display + */ + boolean isAodShowing() { + final boolean isAodShowing = mRootWindowContainer.mTaskSupervisor + .getKeyguardController().isAodShowing(mDisplayId); + if (mDisplayId == DEFAULT_DISPLAY && isAodShowing) { + return !isKeyguardGoingAway(); + } + return isAodShowing; + } + /** * @return whether keyguard is going away on this display */ @@ -7125,14 +7109,19 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } /** + * @return an integer as the changed requested visible insets types. * @see #getRequestedVisibleTypes() */ - void updateRequestedVisibleTypes(@InsetsType int visibleTypes, @InsetsType int mask) { - int newRequestedVisibleTypes = + @InsetsType int updateRequestedVisibleTypes( + @InsetsType int visibleTypes, @InsetsType int mask) { + final int newRequestedVisibleTypes = (mRequestedVisibleTypes & ~mask) | (visibleTypes & mask); if (mRequestedVisibleTypes != newRequestedVisibleTypes) { + final int changedTypes = mRequestedVisibleTypes ^ newRequestedVisibleTypes; mRequestedVisibleTypes = newRequestedVisibleTypes; + return changedTypes; } + return 0; } } diff --git a/services/core/java/com/android/server/wm/EmbeddedWindowController.java b/services/core/java/com/android/server/wm/EmbeddedWindowController.java index 907d0dc2e183..7b6fc9e5694d 100644 --- a/services/core/java/com/android/server/wm/EmbeddedWindowController.java +++ b/services/core/java/com/android/server/wm/EmbeddedWindowController.java @@ -34,6 +34,7 @@ import android.util.proto.ProtoOutputStream; import android.view.InputApplicationHandle; import android.view.InputChannel; import android.view.WindowInsets; +import android.view.WindowInsets.Type.InsetsType; import android.window.InputTransferToken; import com.android.internal.protolog.ProtoLog; @@ -260,7 +261,7 @@ class EmbeddedWindowController { // The EmbeddedWindow can only request the IME. All other insets types are requested by // the host window. - private @WindowInsets.Type.InsetsType int mRequestedVisibleTypes = 0; + private @InsetsType int mRequestedVisibleTypes = 0; /** Whether the gesture is transferred to embedded window. */ boolean mGestureToEmbedded = false; @@ -354,24 +355,28 @@ class EmbeddedWindowController { } @Override - public boolean isRequestedVisible(@WindowInsets.Type.InsetsType int types) { + public boolean isRequestedVisible(@InsetsType int types) { return (mRequestedVisibleTypes & types) != 0; } @Override - public @WindowInsets.Type.InsetsType int getRequestedVisibleTypes() { + public @InsetsType int getRequestedVisibleTypes() { return mRequestedVisibleTypes; } /** * Only the IME can be requested from the EmbeddedWindow. - * @param requestedVisibleTypes other types than {@link WindowInsets.Type.IME} are + * @param requestedVisibleTypes other types than {@link WindowInsets.Type#ime()} are * not sent to system server via WindowlessWindowManager. + * @return an integer as the changed requested visible insets types. */ - void setRequestedVisibleTypes(@WindowInsets.Type.InsetsType int requestedVisibleTypes) { + @InsetsType int setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes) { if (mRequestedVisibleTypes != requestedVisibleTypes) { + final int changedTypes = mRequestedVisibleTypes ^ requestedVisibleTypes; mRequestedVisibleTypes = requestedVisibleTypes; + return changedTypes; } + return 0; } @Override diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java index cf16204f93a1..f52446ff494c 100644 --- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java @@ -282,7 +282,14 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider { // TODO(b/353463205) investigate if we should fail the statsToken, or if it's only // temporary null. if (target != null) { - invokeOnImeRequestedChangedListener(target.getWindow(), statsToken); + // If insets target is not available (e.g. RemoteInsetsControlTarget), use current + // IME input target to update IME request state. For example, switch from a task + // with showing IME to a split-screen task without showing IME. + InsetsTarget insetsTarget = target.getWindow(); + if (insetsTarget == null && mServerVisible) { + insetsTarget = mDisplayContent.getImeInputTarget(); + } + invokeOnImeRequestedChangedListener(insetsTarget, statsToken); } } } @@ -314,7 +321,6 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider { reportImeDrawnForOrganizerIfNeeded((InsetsControlTarget) caller); } } - changed |= mDisplayContent.onImeInsetsClientVisibilityUpdate(); if (Flags.refactorInsetsController()) { if (changed) { ImeTracker.forLogging().onProgress(statsToken, diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java index 4bcba13448e9..b4d55a160631 100644 --- a/services/core/java/com/android/server/wm/InsetsPolicy.java +++ b/services/core/java/com/android/server/wm/InsetsPolicy.java @@ -387,22 +387,6 @@ class InsetsPolicy { state.addSource(navSource); } return state; - } else if (w.mActivityRecord != null && w.mActivityRecord.mImeInsetsFrozenUntilStartInput) { - // During switching tasks with gestural navigation, before the next IME input target - // starts the input, we should adjust and freeze the last IME visibility of the window - // in case delivering obsoleted IME insets state during transitioning. - final InsetsSource originalImeSource = originalState.peekSource(ID_IME); - - if (originalImeSource != null) { - final boolean imeVisibility = w.isRequestedVisible(Type.ime()); - final InsetsState state = copyState - ? new InsetsState(originalState) - : originalState; - final InsetsSource imeSource = new InsetsSource(originalImeSource); - imeSource.setVisible(imeVisibility); - state.addSource(imeSource); - return state; - } } else if (w.mImeInsetsConsumed) { // Set the IME source (if there is one) to be invisible if it has been consumed. final InsetsSource originalImeSource = originalState.peekSource(ID_IME); @@ -453,9 +437,9 @@ class InsetsPolicy { return originalState; } - void onRequestedVisibleTypesChanged(InsetsTarget caller, + void onRequestedVisibleTypesChanged(InsetsTarget caller, @InsetsType int changedTypes, @Nullable ImeTracker.Token statsToken) { - mStateController.onRequestedVisibleTypesChanged(caller, statsToken); + mStateController.onRequestedVisibleTypesChanged(caller, changedTypes, statsToken); checkAbortTransient(caller); updateBarControlTarget(mFocusedWin); } diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java index 9202cf2d5792..164abab992d8 100644 --- a/services/core/java/com/android/server/wm/InsetsStateController.java +++ b/services/core/java/com/android/server/wm/InsetsStateController.java @@ -219,14 +219,20 @@ class InsetsStateController { } } - void onRequestedVisibleTypesChanged(InsetsTarget caller, + void onRequestedVisibleTypesChanged(InsetsTarget caller, @InsetsType int changedTypes, @Nullable ImeTracker.Token statsToken) { boolean changed = false; for (int i = mProviders.size() - 1; i >= 0; i--) { final InsetsSourceProvider provider = mProviders.valueAt(i); - final boolean isImeProvider = provider.getSource().getType() == WindowInsets.Type.ime(); - changed |= provider.updateClientVisibility(caller, - isImeProvider ? statsToken : null); + final @InsetsType int type = provider.getSource().getType(); + if ((type & changedTypes) != 0) { + final boolean isImeProvider = type == WindowInsets.Type.ime(); + changed |= provider.updateClientVisibility( + caller, isImeProvider ? statsToken : null) + // Fake control target cannot change the client visibility, but it should + // change the insets with its newly requested visibility. + || (caller == provider.getFakeControlTarget()); + } } if (changed) { notifyInsetsChanged(); @@ -435,7 +441,8 @@ class InsetsStateController { for (int i = newControlTargets.size() - 1; i >= 0; i--) { // TODO(b/353463205) the statsToken shouldn't be null as it is used later in the // IME provider. Check if we have to create a new request here - onRequestedVisibleTypesChanged(newControlTargets.valueAt(i), null /* statsToken */); + onRequestedVisibleTypesChanged(newControlTargets.valueAt(i), + WindowInsets.Type.all(), null /* statsToken */); } newControlTargets.clear(); if (!android.view.inputmethod.Flags.refactorInsetsController()) { diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java index 6091b8334438..dd2f49e171a8 100644 --- a/services/core/java/com/android/server/wm/KeyguardController.java +++ b/services/core/java/com/android/server/wm/KeyguardController.java @@ -18,6 +18,7 @@ package com.android.server.wm; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION; @@ -216,6 +217,9 @@ class KeyguardController { } else if (keyguardShowing && !state.mKeyguardShowing) { transition.addFlag(TRANSIT_FLAG_KEYGUARD_APPEARING); } + if (mWindowManager.mFlags.mAodTransition && aodShowing && !state.mAodShowing) { + transition.addFlag(TRANSIT_FLAG_AOD_APPEARING); + } } } // Update the task snapshot if the screen will not be turned off. To make sure that the @@ -238,19 +242,27 @@ class KeyguardController { state.mAodShowing = aodShowing; state.writeEventLog("setKeyguardShown"); - if (keyguardChanged) { - // Irrelevant to AOD. - state.mKeyguardGoingAway = false; - if (keyguardShowing) { - state.mDismissalRequested = false; + if (keyguardChanged || aodChanged) { + if (keyguardChanged) { + // Irrelevant to AOD. + state.mKeyguardGoingAway = false; + if (keyguardShowing) { + state.mDismissalRequested = false; + } } if (goingAwayRemoved - || (keyguardShowing && !Display.isOffState(dc.getDisplayInfo().state))) { + || (keyguardShowing && !Display.isOffState(dc.getDisplayInfo().state)) + || (mWindowManager.mFlags.mAodTransition && aodShowing)) { // Keyguard decided to show or stopped going away. Send a transition to animate back // to the locked state before holding the sleep token again if (!ENABLE_NEW_KEYGUARD_SHELL_TRANSITIONS) { dc.requestTransitionAndLegacyPrepare( TRANSIT_TO_FRONT, TRANSIT_FLAG_KEYGUARD_APPEARING); + if (mWindowManager.mFlags.mAodTransition && aodShowing + && dc.mTransitionController.isCollecting()) { + dc.mTransitionController.getCollectingTransition().addFlag( + TRANSIT_FLAG_AOD_APPEARING); + } } dc.mWallpaperController.adjustWallpaperWindows(); dc.executeAppTransition(); diff --git a/services/core/java/com/android/server/wm/PresentationController.java b/services/core/java/com/android/server/wm/PresentationController.java new file mode 100644 index 000000000000..69463433827f --- /dev/null +++ b/services/core/java/com/android/server/wm/PresentationController.java @@ -0,0 +1,86 @@ +/* + * 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.server.wm; + +import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays; + +import android.annotation.NonNull; +import android.util.IntArray; + +import com.android.internal.protolog.ProtoLog; +import com.android.internal.protolog.WmProtoLogGroups; + +/** + * Manages presentation windows. + */ +class PresentationController { + + // TODO(b/395475549): Add support for display add/remove, and activity move across displays. + private final IntArray mPresentingDisplayIds = new IntArray(); + + PresentationController() {} + + private boolean isPresenting(int displayId) { + return mPresentingDisplayIds.contains(displayId); + } + + boolean shouldOccludeActivities(int displayId) { + // All activities on the presenting display must be hidden so that malicious apps can't do + // tap jacking (b/391466268). + // For now, this should only be applied to external displays because presentations can only + // be shown on them. + // TODO(b/390481621): Disallow a presentation from covering its controlling activity so that + // the presentation won't stop its controlling activity. + return enablePresentationForConnectedDisplays() && isPresenting(displayId); + } + + void onPresentationAdded(@NonNull WindowState win) { + final int displayId = win.getDisplayId(); + if (isPresenting(displayId)) { + return; + } + ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION, "Presentation added to display %d: %s", + win.getDisplayId(), win); + mPresentingDisplayIds.add(win.getDisplayId()); + if (enablePresentationForConnectedDisplays()) { + // A presentation hides all activities behind on the same display. + win.mDisplayContent.ensureActivitiesVisible(/*starting=*/ null, + /*notifyClients=*/ true); + } + win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ true); + } + + void onPresentationRemoved(@NonNull WindowState win) { + final int displayId = win.getDisplayId(); + if (!isPresenting(displayId)) { + return; + } + ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION, + "Presentation removed from display %d: %s", win.getDisplayId(), win); + // TODO(b/393945496): Make sure that there's one presentation at most per display. + final int displayIdIndex = mPresentingDisplayIds.indexOf(displayId); + if (displayIdIndex != -1) { + mPresentingDisplayIds.remove(displayIdIndex); + } + if (enablePresentationForConnectedDisplays()) { + // A presentation hides all activities behind on the same display. + win.mDisplayContent.ensureActivitiesVisible(/*starting=*/ null, + /*notifyClients=*/ true); + } + win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ false); + } +} diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index 1ad5988e3c2e..8d198b26f396 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -704,9 +704,10 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { ImeTracker.forLogging().onProgress(imeStatsToken, ImeTracker.PHASE_WM_UPDATE_REQUESTED_VISIBLE_TYPES); } - win.setRequestedVisibleTypes(requestedVisibleTypes); + final @InsetsType int changedTypes = + win.setRequestedVisibleTypes(requestedVisibleTypes); win.getDisplayContent().getInsetsPolicy().onRequestedVisibleTypesChanged(win, - imeStatsToken); + changedTypes, imeStatsToken); final Task task = win.getTask(); if (task != null) { task.dispatchTaskInfoChangedIfNeeded(/* forced= */ true); @@ -723,10 +724,11 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { // TODO(b/353463205) Use different phase here ImeTracker.forLogging().onProgress(imeStatsToken, ImeTracker.PHASE_WM_UPDATE_REQUESTED_VISIBLE_TYPES); - embeddedWindow.setRequestedVisibleTypes( + final @InsetsType int changedTypes = embeddedWindow.setRequestedVisibleTypes( requestedVisibleTypes & WindowInsets.Type.ime()); embeddedWindow.getDisplayContent().getInsetsPolicy() - .onRequestedVisibleTypesChanged(embeddedWindow, imeStatsToken); + .onRequestedVisibleTypesChanged( + embeddedWindow, changedTypes, imeStatsToken); } else { ImeTracker.forLogging().onFailed(imeStatsToken, ImeTracker.PHASE_WM_UPDATE_REQUESTED_VISIBLE_TYPES); diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java index cc14383fc9f9..ae3a015a690d 100644 --- a/services/core/java/com/android/server/wm/TaskDisplayArea.java +++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java @@ -460,7 +460,7 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { // If the previous front-most task is moved to the back, then notify of the new // front-most task. - final ActivityRecord topMost = getTopMostActivity(); + final ActivityRecord topMost = getTopNonFinishingActivity(); if (topMost != null) { mAtmService.getTaskChangeNotificationController().notifyTaskMovedToFront( topMost.getTask().getTaskInfo()); diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 64105f634f84..324852d1a410 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -1224,6 +1224,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { false /* ignoringKeyguard */, true /* ignoringInvisibleActivity */); } + @Override ActivityRecord getTopNonFinishingActivity() { return getTopNonFinishingActivity( true /* includeOverlays */, true /* includeLaunchedFromBubble */); diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 5217a759c6ae..fe653e454d6c 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -36,6 +36,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING; import static android.view.WindowManager.TRANSIT_FLAG_IS_RECENTS; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED; import static android.view.WindowManager.TRANSIT_OPEN; @@ -973,6 +974,10 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { return false; } + boolean isInAodAppearTransition() { + return (mFlags & TRANSIT_FLAG_AOD_APPEARING) != 0; + } + /** * Specifies configuration change explicitly for the window container, so it can be chosen as * transition target. This is usually used with transition mode diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index ff9e5a2aad99..25b513d85384 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -525,6 +525,19 @@ class TransitionController { return false; } + boolean isInAodAppearTransition() { + if (mCollectingTransition != null && mCollectingTransition.isInAodAppearTransition()) { + return true; + } + for (int i = mWaitingTransitions.size() - 1; i >= 0; --i) { + if (mWaitingTransitions.get(i).isInAodAppearTransition()) return true; + } + for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) { + if (mPlayingTransitions.get(i).isInAodAppearTransition()) return true; + } + return false; + } + /** * @return A pair of the transition and restore-behind target for the given {@param container}. * @param container An ancestor of a transient-launch activity @@ -542,6 +555,23 @@ class TransitionController { return null; } + /** + * @return The playing transition that is transiently-hiding the given {@param container}, or + * null if there isn't one + * @param container A participant of a transient-hide transition + */ + @Nullable + Transition getTransientHideTransitionForContainer( + @NonNull WindowContainer container) { + for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) { + final Transition transition = mPlayingTransitions.get(i); + if (transition.isInTransientHide(container)) { + return transition; + } + } + return null; + } + /** Returns {@code true} if the display contains a transient-launch transition. */ boolean hasTransientLaunch(@NonNull DisplayContent dc) { if (mCollectingTransition != null && mCollectingTransition.hasTransientLaunch() diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index c1ef208d1d4d..70948e1264c4 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -166,6 +166,14 @@ class WallpaperController { mFindResults.setWallpaperTarget(w); return false; } + } else if (mService.mFlags.mAodTransition + && mDisplayContent.isKeyguardLockedOrAodShowing()) { + if (mService.mPolicy.isKeyguardHostWindow(w.mAttrs) + && w.mTransitionController.isInAodAppearTransition()) { + if (DEBUG_WALLPAPER) Slog.v(TAG, "Found aod transition wallpaper target: " + w); + mFindResults.setWallpaperTarget(w); + return true; + } } final boolean animationWallpaper = animatingContainer != null @@ -684,7 +692,8 @@ class WallpaperController { private WallpaperWindowToken getTokenForTarget(WindowState target) { if (target == null) return null; WindowState window = mFindResults.getTopWallpaper( - target.canShowWhenLocked() && mService.isKeyguardLocked()); + (target.canShowWhenLocked() && mService.isKeyguardLocked()) + || (mService.mFlags.mAodTransition && mDisplayContent.isAodShowing())); return window == null ? null : window.mToken.asWallpaperToken(); } @@ -727,7 +736,9 @@ class WallpaperController { if (mFindResults.wallpaperTarget == null && mFindResults.useTopWallpaperAsTarget) { mFindResults.setWallpaperTarget( - mFindResults.getTopWallpaper(mDisplayContent.isKeyguardLocked())); + mFindResults.getTopWallpaper(mService.mFlags.mAodTransition + ? mDisplayContent.isKeyguardLockedOrAodShowing() + : mDisplayContent.isKeyguardLocked())); } } @@ -899,11 +910,17 @@ class WallpaperController { if (mDisplayContent.mWmService.mFlags.mEnsureWallpaperInTransitions) { visibleRequested = mWallpaperTarget != null && mWallpaperTarget.isVisibleRequested(); } - updateWallpaperTokens(visibleRequested, mDisplayContent.isKeyguardLocked()); + updateWallpaperTokens(visibleRequested, + mService.mFlags.mAodTransition + ? mDisplayContent.isKeyguardLockedOrAodShowing() + : mDisplayContent.isKeyguardLocked()); ProtoLog.v(WM_DEBUG_WALLPAPER, "Wallpaper at display %d - visibility: %b, keyguardLocked: %b", - mDisplayContent.getDisplayId(), visible, mDisplayContent.isKeyguardLocked()); + mDisplayContent.getDisplayId(), visible, + mService.mFlags.mAodTransition + ? mDisplayContent.isKeyguardLockedOrAodShowing() + : mDisplayContent.isKeyguardLocked()); if (visible && mLastFrozen != mFindResults.isWallpaperTargetForLetterbox) { mLastFrozen = mFindResults.isWallpaperTargetForLetterbox; diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 225951dbd345..55c2668f62d0 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -2079,6 +2079,10 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< return getActivity(alwaysTruePredicate(), true /* traverseTopToBottom */); } + ActivityRecord getTopNonFinishingActivity() { + return getActivity(r -> !r.finishing, true /* traverseTopToBottom */); + } + ActivityRecord getTopActivity(boolean includeFinishing, boolean includeOverlays) { // Break down into 4 calls to avoid object creation due to capturing input params. if (includeFinishing) { diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 04ddb3cacf27..bb669915e366 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -157,7 +157,6 @@ import static com.android.server.wm.WindowManagerServiceDumpProto.POLICY; import static com.android.server.wm.WindowManagerServiceDumpProto.ROOT_WINDOW_CONTAINER; import static com.android.server.wm.WindowManagerServiceDumpProto.WINDOW_FRAMES_VALID; import static com.android.window.flags.Flags.enableDisplayFocusInShellTransitions; -import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays; import static com.android.window.flags.Flags.multiCrop; import static com.android.window.flags.Flags.setScPropertiesInClient; @@ -503,6 +502,8 @@ public class WindowManagerService extends IWindowManager.Stub final StartingSurfaceController mStartingSurfaceController; + final PresentationController mPresentationController; + private final IVrStateCallbacks mVrStateCallbacks = new IVrStateCallbacks.Stub() { @Override public void onVrStateChanged(boolean enabled) { @@ -732,8 +733,14 @@ public class WindowManagerService extends IWindowManager.Stub new WallpaperVisibilityListeners(); IDisplayChangeWindowController mDisplayChangeController = null; - private final DeathRecipient mDisplayChangeControllerDeath = - () -> mDisplayChangeController = null; + private final DeathRecipient mDisplayChangeControllerDeath = new DeathRecipient() { + @Override + public void binderDied() { + synchronized (mGlobalLock) { + mDisplayChangeController = null; + } + } + }; final DisplayWindowListenerController mDisplayNotificationController; final TaskSystemBarsListenerController mTaskSystemBarsListenerController; @@ -1427,6 +1434,7 @@ public class WindowManagerService extends IWindowManager.Stub setGlobalShadowSettings(); mAnrController = new AnrController(this); mStartingSurfaceController = new StartingSurfaceController(this); + mPresentationController = new PresentationController(); mBlurController = new BlurController(mContext, mPowerManager); mTaskFpsCallbackController = new TaskFpsCallbackController(mContext); @@ -1931,16 +1939,8 @@ public class WindowManagerService extends IWindowManager.Stub } outSizeCompatScale[0] = win.getCompatScaleForClient(); - if (res >= ADD_OKAY - && (type == TYPE_PRESENTATION || type == TYPE_PRIVATE_PRESENTATION)) { - displayContent.mIsPresenting = true; - if (enablePresentationForConnectedDisplays()) { - // A presentation hides all activities behind on the same display. - displayContent.ensureActivitiesVisible(/*starting=*/ null, - /*notifyClients=*/ true); - } - mDisplayManagerInternal.onPresentation(displayContent.getDisplay().getDisplayId(), - /*isShown=*/ true); + if (res >= ADD_OKAY && win.isPresentation()) { + mPresentationController.onPresentationAdded(win); } } @@ -4726,11 +4726,13 @@ public class WindowManagerService extends IWindowManager.Stub } ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_WM_UPDATE_DISPLAY_WINDOW_REQUESTED_VISIBLE_TYPES); - dc.mRemoteInsetsControlTarget.updateRequestedVisibleTypes(visibleTypes, mask); + final @InsetsType int changedTypes = + dc.mRemoteInsetsControlTarget.updateRequestedVisibleTypes( + visibleTypes, mask); // TODO(b/353463205) the statsToken shouldn't be null as it is used later in the // IME provider. Check if we have to create a new request here, if null. dc.getInsetsStateController().onRequestedVisibleTypesChanged( - dc.mRemoteInsetsControlTarget, statsToken); + dc.mRemoteInsetsControlTarget, changedTypes, statsToken); } } finally { Binder.restoreCallingIdentity(origId); diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index a11f4b1f3fc3..924b9de5a562 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -702,9 +702,23 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub if ((entry.getValue().getChangeMask() & WindowContainerTransaction.Change.CHANGE_FORCE_NO_PIP) != 0) { - // Disable entering pip (eg. when recents pretends to finish itself) - if (chain.mTransition != null) { - chain.mTransition.setCanPipOnFinish(false /* canPipOnFinish */); + if (com.android.wm.shell.Flags.enableRecentsBookendTransition()) { + // If we are using a bookend transition, then the transition that we need + // to disable pip on finish is the original transient transition, not the + // bookend transition + final Transition transientHideTransition = + mTransitionController.getTransientHideTransitionForContainer(wc); + if (transientHideTransition != null) { + transientHideTransition.setCanPipOnFinish(false); + } else { + ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS, + "Set do-not-pip: no task"); + } + } else { + // Disable entering pip (eg. when recents pretends to finish itself) + if (chain.mTransition != null) { + chain.mTransition.setCanPipOnFinish(false /* canPipOnFinish */); + } } } // A bit hacky, but we need to detect "remove PiP" so that we can "wrap" the diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 84d8f840d849..589724182980 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -182,7 +182,6 @@ import static com.android.server.wm.WindowStateProto.UNRESTRICTED_KEEP_CLEAR_ARE import static com.android.server.wm.WindowStateProto.VIEW_VISIBILITY; import static com.android.server.wm.WindowStateProto.WINDOW_CONTAINER; import static com.android.server.wm.WindowStateProto.WINDOW_FRAMES; -import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays; import static com.android.window.flags.Flags.surfaceTrustedOverlay; import android.annotation.CallSuper; @@ -822,17 +821,23 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } /** + * @return an integer as the changed requested visible insets types. * @see #getRequestedVisibleTypes() */ - void setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes) { + @InsetsType int setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes) { if (mRequestedVisibleTypes != requestedVisibleTypes) { + final int changedTypes = mRequestedVisibleTypes ^ requestedVisibleTypes; mRequestedVisibleTypes = requestedVisibleTypes; + return changedTypes; } + return 0; } @VisibleForTesting - void setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes, @InsetsType int mask) { - setRequestedVisibleTypes(mRequestedVisibleTypes & ~mask | requestedVisibleTypes & mask); + @InsetsType int setRequestedVisibleTypes( + @InsetsType int requestedVisibleTypes, @InsetsType int mask) { + return setRequestedVisibleTypes( + mRequestedVisibleTypes & ~mask | requestedVisibleTypes & mask); } /** @@ -2069,38 +2074,15 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP super.onMovedByResize(); } - void onAppVisibilityChanged(boolean visible, boolean runningAppAnimation) { + void onAppCommitInvisible() { for (int i = mChildren.size() - 1; i >= 0; --i) { - mChildren.get(i).onAppVisibilityChanged(visible, runningAppAnimation); + mChildren.get(i).onAppCommitInvisible(); } - - final boolean isVisibleNow = isVisibleNow(); - if (mAttrs.type == TYPE_APPLICATION_STARTING) { - // Starting window that's exiting will be removed when the animation finishes. - // Mark all relevant flags for that onExitAnimationDone will proceed all the way - // to actually remove it. - if (!visible && isVisibleNow && mActivityRecord.isAnimating(PARENTS | TRANSITION)) { - ProtoLog.d(WM_DEBUG_ANIM, - "Set animatingExit: reason=onAppVisibilityChanged win=%s", this); - mAnimatingExit = true; - mRemoveOnExit = true; - mWindowRemovalAllowed = true; - } - } else if (visible != isVisibleNow) { - // Run exit animation if: - // 1. App visibility and WS visibility are different - // 2. App is not running an animation - // 3. WS is currently visible - if (!runningAppAnimation && isVisibleNow) { - final AccessibilityController accessibilityController = - mWmService.mAccessibilityController; - final int winTransit = TRANSIT_EXIT; - mWinAnimator.applyAnimationLocked(winTransit, false /* isEntrance */); - if (accessibilityController.hasCallbacks()) { - accessibilityController.onWindowTransition(this, winTransit); - } - } - setDisplayLayoutNeeded(); + if (mAttrs.type != TYPE_APPLICATION_STARTING + && mWmService.mAccessibilityController.hasCallbacks() + // It is a change only if App visibility and WS visibility are different. + && isVisible()) { + mWmService.mAccessibilityController.onWindowTransition(this, TRANSIT_EXIT); } } @@ -2317,15 +2299,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP final int type = mAttrs.type; - if (type == TYPE_PRESENTATION || type == TYPE_PRIVATE_PRESENTATION) { - // TODO(b/393945496): Make sure that there's one presentation at most per display. - dc.mIsPresenting = false; - if (enablePresentationForConnectedDisplays()) { - // A presentation hides all activities behind on the same display. - dc.ensureActivitiesVisible(/*starting=*/ null, /*notifyClients=*/ true); - } - mWmService.mDisplayManagerInternal.onPresentation(dc.getDisplay().getDisplayId(), - /*isShown=*/ false); + if (isPresentation()) { + mWmService.mPresentationController.onPresentationRemoved(this); } // Check if window provides non decor insets before clearing its provided insets. final boolean windowProvidesDisplayDecorInsets = providesDisplayDecorInsets(); @@ -3354,6 +3329,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } } + boolean isPresentation() { + return mAttrs.type == TYPE_PRESENTATION || mAttrs.type == TYPE_PRIVATE_PRESENTATION; + } + private boolean isOnVirtualDisplay() { return getDisplayContent().mDisplay.getType() == Display.TYPE_VIRTUAL; } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java b/services/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java index 6e038f9b67a0..ba02122d1dc5 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java @@ -54,17 +54,7 @@ class EnterpriseSpecificIdCalculator { TelephonyManager telephonyService = context.getSystemService(TelephonyManager.class); Preconditions.checkState(telephonyService != null, "Unable to access telephony service"); - String imei; - try { - imei = telephonyService.getImei(0); - } catch (UnsupportedOperationException doesNotSupportGms) { - // Instead of catching the exception, we could check for FEATURE_TELEPHONY_GSM. - // However that runs the risk of changing a device's existing ESID if on these devices - // telephonyService.getImei() actually returns non-null even when the device does not - // declare FEATURE_TELEPHONY_GSM. - imei = null; - } - mImei = imei; + mImei = telephonyService.getImei(0); String meid; try { meid = telephonyService.getMeid(0); diff --git a/services/incremental/IncrementalService.cpp b/services/incremental/IncrementalService.cpp index dae481a3c215..36947a2a6d62 100644 --- a/services/incremental/IncrementalService.cpp +++ b/services/incremental/IncrementalService.cpp @@ -3198,8 +3198,10 @@ void IncrementalService::DataLoaderStub::onDump(int fd) { dprintf(fd, " }\n"); } -void IncrementalService::AppOpsListener::opChanged(int32_t, const String16&) { +binder::Status IncrementalService::AppOpsListener::opChanged(int32_t, int32_t, + const String16&, const String16&) { incrementalService.onAppOpChanged(packageName); + return binder::Status::ok(); } binder::Status IncrementalService::IncrementalServiceConnector::setStorageParams( diff --git a/services/incremental/IncrementalService.h b/services/incremental/IncrementalService.h index b81e1b1b071c..4ee1a70dc34c 100644 --- a/services/incremental/IncrementalService.h +++ b/services/incremental/IncrementalService.h @@ -26,7 +26,7 @@ #include <android/os/incremental/BnStorageLoadingProgressListener.h> #include <android/os/incremental/PerUidReadTimeouts.h> #include <android/os/incremental/StorageHealthCheckParams.h> -#include <binder/IAppOpsCallback.h> +#include <binder/AppOpsManager.h> #include <binder/PersistableBundle.h> #include <utils/String16.h> #include <utils/StrongPointer.h> @@ -200,11 +200,12 @@ public: void getMetrics(int32_t storageId, android::os::PersistableBundle* _aidl_return); - class AppOpsListener : public android::BnAppOpsCallback { + class AppOpsListener : public com::android::internal::app::BnAppOpsCallback { public: AppOpsListener(IncrementalService& incrementalService, std::string packageName) : incrementalService(incrementalService), packageName(std::move(packageName)) {} - void opChanged(int32_t op, const String16& packageName) final; + binder::Status opChanged(int32_t op, int32_t uid, const String16& packageName, + const String16& persistentDeviceId) final; private: IncrementalService& incrementalService; diff --git a/services/incremental/ServiceWrappers.h b/services/incremental/ServiceWrappers.h index 39e2ee324e0c..36a5b7f4a75d 100644 --- a/services/incremental/ServiceWrappers.h +++ b/services/incremental/ServiceWrappers.h @@ -23,7 +23,7 @@ #include <android/content/pm/IDataLoader.h> #include <android/content/pm/IDataLoaderStatusListener.h> #include <android/os/incremental/PerUidReadTimeouts.h> -#include <binder/IAppOpsCallback.h> +#include <binder/AppOpsManager.h> #include <binder/IServiceManager.h> #include <binder/Status.h> #include <incfs.h> @@ -133,6 +133,7 @@ public: class AppOpsManagerWrapper { public: + using IAppOpsCallback = ::com::android::internal::app::IAppOpsCallback; virtual ~AppOpsManagerWrapper() = default; virtual binder::Status checkPermission(const char* permission, const char* operation, const char* package) const = 0; diff --git a/services/incremental/test/IncrementalServiceTest.cpp b/services/incremental/test/IncrementalServiceTest.cpp index d9d3d62e92e2..73849a3e0e00 100644 --- a/services/incremental/test/IncrementalServiceTest.cpp +++ b/services/incremental/test/IncrementalServiceTest.cpp @@ -1678,7 +1678,7 @@ TEST_F(IncrementalServiceTest, testSetIncFsMountOptionsSuccessAndPermissionChang {}, {})); ASSERT_GE(mDataLoader->setStorageParams(true), 0); ASSERT_NE(nullptr, mAppOpsManager->mStoredCallback.get()); - mAppOpsManager->mStoredCallback->opChanged(0, {}); + mAppOpsManager->mStoredCallback->opChanged(0, 0, {}, {}); } TEST_F(IncrementalServiceTest, testSetIncFsMountOptionsCheckPermissionFails) { diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index c974d9e1dc87..2bbd69c65eb8 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -894,6 +894,17 @@ public final class SystemServer implements Dumpable { SystemServiceRegistry.sEnableServiceNotFoundWtf = true; + // Prepare the thread pool for init tasks that can be parallelized + SystemServerInitThreadPool tp = SystemServerInitThreadPool.start(); + mDumper.addDumpable(tp); + + if (android.server.Flags.earlySystemConfigInit()) { + // SystemConfig init is expensive, so enqueue the work as early as possible to allow + // concurrent execution before it's needed (typically by ActivityManagerService). + // As native library loading is also expensive, this is a good place to start. + startSystemConfigInit(t); + } + // Initialize native services. System.loadLibrary("android_servers"); @@ -926,9 +937,6 @@ public final class SystemServer implements Dumpable { mDumper.addDumpable(mSystemServiceManager); LocalServices.addService(SystemServiceManager.class, mSystemServiceManager); - // Prepare the thread pool for init tasks that can be parallelized - SystemServerInitThreadPool tp = SystemServerInitThreadPool.start(); - mDumper.addDumpable(tp); // Lazily load the pre-installed system font map in SystemServer only if we're not doing // the optimized font loading in the FontManagerService. @@ -1093,6 +1101,14 @@ public final class SystemServer implements Dumpable { } } + private void startSystemConfigInit(TimingsTraceAndSlog t) { + Slog.i(TAG, "Reading configuration..."); + final String tagSystemConfig = "ReadingSystemConfig"; + t.traceBegin(tagSystemConfig); + SystemServerInitThreadPool.submit(SystemConfig::getInstance, tagSystemConfig); + t.traceEnd(); + } + private void createSystemContext() { ActivityThread activityThread = ActivityThread.systemMain(); mSystemContext = activityThread.getSystemContext(); @@ -1131,11 +1147,11 @@ public final class SystemServer implements Dumpable { mDumper.addDumpable(watchdog); t.traceEnd(); - Slog.i(TAG, "Reading configuration..."); - final String TAG_SYSTEM_CONFIG = "ReadingSystemConfig"; - t.traceBegin(TAG_SYSTEM_CONFIG); - SystemServerInitThreadPool.submit(SystemConfig::getInstance, TAG_SYSTEM_CONFIG); - t.traceEnd(); + // Legacy entry point for starting SystemConfig init, only needed if the early init flag is + // disabled and we haven't already triggered init before bootstrap services. + if (!android.server.Flags.earlySystemConfigInit()) { + startSystemConfigInit(t); + } // Orchestrates some ProtoLogging functionality. if (android.tracing.Flags.clientSideProtoLogging()) { diff --git a/services/java/com/android/server/flags.aconfig b/services/java/com/android/server/flags.aconfig index 4d021ec2c0d3..86ccd878de7c 100644 --- a/services/java/com/android/server/flags.aconfig +++ b/services/java/com/android/server/flags.aconfig @@ -10,6 +10,13 @@ flag { } flag { + namespace: "system_performance" + name: "early_system_config_init" + description: "Perform earlier initialization of SystemConfig in system server startup." + bug: "383869534" +} + +flag { name: "remove_text_service" namespace: "wear_frameworks" description: "Remove TextServiceManagerService on Wear" diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java index a103b0583eac..a7280c2167ea 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java @@ -31,7 +31,6 @@ import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; import android.app.Instrumentation; -import android.content.Context; import android.content.res.Configuration; import android.graphics.Insets; import android.os.RemoteException; @@ -42,7 +41,6 @@ import android.provider.Settings; import android.server.wm.WindowManagerStateHelper; import android.util.Log; import android.view.WindowManagerGlobal; -import android.view.WindowManagerPolicyConstants; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.Flags; import android.view.inputmethod.InputMethodManager; @@ -59,6 +57,7 @@ import androidx.test.uiautomator.Until; import com.android.apps.inputmethod.simpleime.ims.InputMethodServiceWrapper; import com.android.apps.inputmethod.simpleime.testing.TestActivity; +import com.android.compatibility.common.util.GestureNavSwitchHelper; import com.android.compatibility.common.util.SystemUtil; import org.junit.After; @@ -90,6 +89,8 @@ public class InputMethodServiceTest { private final WindowManagerStateHelper mWmState = new WindowManagerStateHelper(); + private final GestureNavSwitchHelper mGestureNavSwitchHelper = new GestureNavSwitchHelper(); + private final DeviceFlagsValueProvider mFlagsValueProvider = new DeviceFlagsValueProvider(); @Rule @@ -100,7 +101,6 @@ public class InputMethodServiceTest { private Instrumentation mInstrumentation; private UiDevice mUiDevice; - private Context mContext; private InputMethodManager mImm; private String mTargetPackageName; private String mInputMethodId; @@ -112,8 +112,7 @@ public class InputMethodServiceTest { public void setUp() throws Exception { mInstrumentation = InstrumentationRegistry.getInstrumentation(); mUiDevice = UiDevice.getInstance(mInstrumentation); - mContext = mInstrumentation.getContext(); - mImm = mContext.getSystemService(InputMethodManager.class); + mImm = mInstrumentation.getContext().getSystemService(InputMethodManager.class); mTargetPackageName = mInstrumentation.getTargetContext().getPackageName(); mInputMethodId = getInputMethodId(); prepareIme(); @@ -872,35 +871,47 @@ public class InputMethodServiceTest { * Verifies that clicking on the IME navigation bar back button hides the IME. */ @Test - public void testBackButtonClick() { + public void testBackButtonClick() throws Exception { assumeTrue("Must have a navigation bar", hasNavigationBar()); - assumeTrue("Must be in gesture navigation mode", isGestureNavEnabled()); waitUntilActivityReadyForInputInjection(mActivity); setShowImeWithHardKeyboard(true /* enabled */); - verifyInputViewStatusOnMainSync( - () -> { - setDrawsImeNavBarAndSwitcherButton(true /* enabled */); - mActivity.showImeWithWindowInsetsController(); - }, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + final boolean isGestureMode = mGestureNavSwitchHelper.isGestureMode(); - final var backButtonUiObject = getUiObject(By.res(INPUT_METHOD_NAV_BACK_ID)); - backButtonUiObject.click(); - mInstrumentation.waitForIdleSync(); + final var restoreNav = new AutoCloseable[]{() -> {}}; + try { + if (!isGestureMode) { + // Wait for onConfigurationChanged when changing navigation modes. + verifyInputViewStatus( + () -> restoreNav[0] = mGestureNavSwitchHelper.withGestureNavigationMode(), + true, /* expected */ + false /* inputViewStarted */ + ); + } - if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { - // The IME visibility is only sent at the end of the animation. Therefore, we have to - // wait until the visibility was sent to the server and the IME window hidden. - eventually(() -> assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse()); - } else { - assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse(); + verifyInputViewStatusOnMainSync( + () -> mActivity.showImeWithWindowInsetsController(), + true /* expected */, + true /* inputViewStarted */); + assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + + final var backButton = getUiObject(By.res(INPUT_METHOD_NAV_BACK_ID)); + backButton.click(); + mInstrumentation.waitForIdleSync(); + + if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + // The IME visibility is only sent at the end of the animation. Therefore, we have + // to wait until the visibility was sent to the server and the IME window hidden. + eventually(() -> assertWithMessage("IME is not shown") + .that(mInputMethodService.isInputViewShown()).isFalse()); + } else { + assertWithMessage("IME is not shown") + .that(mInputMethodService.isInputViewShown()).isFalse(); + } + } finally { + restoreNav[0].close(); } } @@ -908,35 +919,47 @@ public class InputMethodServiceTest { * Verifies that long clicking on the IME navigation bar back button hides the IME. */ @Test - public void testBackButtonLongClick() { + public void testBackButtonLongClick() throws Exception { assumeTrue("Must have a navigation bar", hasNavigationBar()); - assumeTrue("Must be in gesture navigation mode", isGestureNavEnabled()); waitUntilActivityReadyForInputInjection(mActivity); setShowImeWithHardKeyboard(true /* enabled */); - verifyInputViewStatusOnMainSync( - () -> { - setDrawsImeNavBarAndSwitcherButton(true /* enabled */); - mActivity.showImeWithWindowInsetsController(); - }, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + final boolean isGestureMode = mGestureNavSwitchHelper.isGestureMode(); - final var backButtonUiObject = getUiObject(By.res(INPUT_METHOD_NAV_BACK_ID)); - backButtonUiObject.longClick(); - mInstrumentation.waitForIdleSync(); + final var restoreNav = new AutoCloseable[]{() -> {}}; + try { + if (!isGestureMode) { + // Wait for onConfigurationChanged when changing navigation modes. + verifyInputViewStatus( + () -> restoreNav[0] = mGestureNavSwitchHelper.withGestureNavigationMode(), + true, /* expected */ + false /* inputViewStarted */ + ); + } - if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { - // The IME visibility is only sent at the end of the animation. Therefore, we have to - // wait until the visibility was sent to the server and the IME window hidden. - eventually(() -> assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse()); - } else { - assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse(); + verifyInputViewStatusOnMainSync( + () -> mActivity.showImeWithWindowInsetsController(), + true /* expected */, + true /* inputViewStarted */); + assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + + final var backButton = getUiObject(By.res(INPUT_METHOD_NAV_BACK_ID)); + backButton.longClick(); + mInstrumentation.waitForIdleSync(); + + if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + // The IME visibility is only sent at the end of the animation. Therefore, we have + // to wait until the visibility was sent to the server and the IME window hidden. + eventually(() -> assertWithMessage("IME is not shown") + .that(mInputMethodService.isInputViewShown()).isFalse()); + } else { + assertWithMessage("IME is not shown") + .that(mInputMethodService.isInputViewShown()).isFalse(); + } + } finally { + restoreNav[0].close(); } } @@ -945,74 +968,104 @@ public class InputMethodServiceTest { * or switches the input method. */ @Test - public void testImeSwitchButtonClick() { + public void testImeSwitchButtonClick() throws Exception { assumeTrue("Must have a navigation bar", hasNavigationBar()); - assumeTrue("Must be in gesture navigation mode", isGestureNavEnabled()); waitUntilActivityReadyForInputInjection(mActivity); setShowImeWithHardKeyboard(true /* enabled */); - verifyInputViewStatusOnMainSync( - () -> { - setDrawsImeNavBarAndSwitcherButton(true /* enabled */); - mActivity.showImeWithWindowInsetsController(); - }, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + final boolean isGestureMode = mGestureNavSwitchHelper.isGestureMode(); - final var initialInfo = mImm.getCurrentInputMethodInfo(); + final var restoreNav = new AutoCloseable[]{() -> {}}; + try { + if (!isGestureMode) { + // Wait for onConfigurationChanged when changing navigation modes. + verifyInputViewStatus( + () -> restoreNav[0] = mGestureNavSwitchHelper.withGestureNavigationMode(), + true, /* expected */ + false /* inputViewStarted */ + ); + } - final var imeSwitchButtonUiObject = getUiObject(By.res(INPUT_METHOD_NAV_IME_SWITCHER_ID)); - imeSwitchButtonUiObject.click(); - mInstrumentation.waitForIdleSync(); + verifyInputViewStatusOnMainSync( + () -> { + setDrawsImeNavBarAndSwitcherButton(true /* enabled */); + mActivity.showImeWithWindowInsetsController(); + }, + true /* expected */, + true /* inputViewStarted */); + assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); - final var newInfo = mImm.getCurrentInputMethodInfo(); + final var initialInfo = mImm.getCurrentInputMethodInfo(); - assertWithMessage("Input Method Switcher Menu is shown or input method was switched") - .that(isInputMethodPickerShown(mImm) || !Objects.equals(initialInfo, newInfo)) - .isTrue(); + final var imeSwitcherButton = getUiObject(By.res(INPUT_METHOD_NAV_IME_SWITCHER_ID)); + imeSwitcherButton.click(); + mInstrumentation.waitForIdleSync(); - assertWithMessage("IME is still shown after IME Switcher button was clicked") - .that(mInputMethodService.isInputViewShown()).isTrue(); + final var newInfo = mImm.getCurrentInputMethodInfo(); + + assertWithMessage("Input Method Switcher Menu is shown or input method was switched") + .that(isInputMethodPickerShown(mImm) || !Objects.equals(initialInfo, newInfo)) + .isTrue(); + + assertWithMessage("IME is still shown after IME Switcher button was clicked") + .that(mInputMethodService.isInputViewShown()).isTrue(); - // Hide the IME Switcher Menu before finishing. - mUiDevice.pressBack(); + // Hide the IME Switcher Menu before finishing. + mUiDevice.pressBack(); + } finally { + restoreNav[0].close(); + } } /** * Verifies that long clicking on the IME switch button shows the Input Method Switcher Menu. */ @Test - public void testImeSwitchButtonLongClick() { + public void testImeSwitchButtonLongClick() throws Exception { assumeTrue("Must have a navigation bar", hasNavigationBar()); - assumeTrue("Must be in gesture navigation mode", isGestureNavEnabled()); waitUntilActivityReadyForInputInjection(mActivity); setShowImeWithHardKeyboard(true /* enabled */); - verifyInputViewStatusOnMainSync( - () -> { - setDrawsImeNavBarAndSwitcherButton(true /* enabled */); - mActivity.showImeWithWindowInsetsController(); - }, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + final boolean isGestureMode = mGestureNavSwitchHelper.isGestureMode(); - final var imeSwitchButtonUiObject = getUiObject(By.res(INPUT_METHOD_NAV_IME_SWITCHER_ID)); - imeSwitchButtonUiObject.longClick(); - mInstrumentation.waitForIdleSync(); + final var restoreNav = new AutoCloseable[]{() -> {}}; + try { + if (!isGestureMode) { + // Wait for onConfigurationChanged when changing navigation modes. + verifyInputViewStatus( + () -> restoreNav[0] = mGestureNavSwitchHelper.withGestureNavigationMode(), + true, /* expected */ + false /* inputViewStarted */ + ); + } - assertWithMessage("Input Method Switcher Menu is shown") - .that(isInputMethodPickerShown(mImm)).isTrue(); - assertWithMessage("IME is still shown after IME Switcher button was long clicked") - .that(mInputMethodService.isInputViewShown()).isTrue(); + verifyInputViewStatusOnMainSync( + () -> { + setDrawsImeNavBarAndSwitcherButton(true /* enabled */); + mActivity.showImeWithWindowInsetsController(); + }, + true /* expected */, + true /* inputViewStarted */); + assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + + final var imeSwitcherButton = getUiObject(By.res(INPUT_METHOD_NAV_IME_SWITCHER_ID)); + imeSwitcherButton.longClick(); + mInstrumentation.waitForIdleSync(); - // Hide the IME Switcher Menu before finishing. - mUiDevice.pressBack(); + assertWithMessage("Input Method Switcher Menu is shown") + .that(isInputMethodPickerShown(mImm)).isTrue(); + assertWithMessage("IME is still shown after IME Switcher button was long clicked") + .that(mInputMethodService.isInputViewShown()).isTrue(); + + // Hide the IME Switcher Menu before finishing. + mUiDevice.pressBack(); + } finally { + restoreNav[0].close(); + } } private void verifyInputViewStatus(@NonNull Runnable runnable, boolean expected, @@ -1105,6 +1158,9 @@ public class InputMethodServiceTest { // Get the new TestActivity. mActivity = TestActivity.getLastCreatedInstance(); assertWithMessage("Re-created activity is not null").that(mActivity).isNotNull(); + // Wait for the new EditText to be served by InputMethodManager. + eventually(() -> assertWithMessage("Has an input connection to the re-created Activity") + .that(mImm.hasActiveInputConnection(mActivity.getEditText())).isTrue()); } verifyInputViewStatusOnMainSync( @@ -1214,18 +1270,12 @@ public class InputMethodServiceTest { return uiObject; } - /** Checks whether gesture navigation move is enabled. */ - private boolean isGestureNavEnabled() { - return mContext.getResources().getInteger( - com.android.internal.R.integer.config_navBarInteractionMode) - == WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; - } - /** Checks whether the device has a navigation bar on the IME's display. */ private boolean hasNavigationBar() { try { return WindowManagerGlobal.getWindowManagerService() - .hasNavigationBar(mInputMethodService.getDisplayId()); + .hasNavigationBar(mInputMethodService.getDisplayId()) + && mGestureNavSwitchHelper.hasNavigationBar(); } catch (RemoteException e) { fail("Failed to check whether the device has a navigation bar: " + e.getMessage()); return false; diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml index cf7d660a68ef..00873de4aaed 100644 --- a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml +++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml @@ -42,6 +42,7 @@ <activity android:name="com.android.apps.inputmethod.simpleime.testing.TestActivity" android:exported="false" android:label="TestActivity" + android:configChanges="assetsPaths" android:launchMode="singleInstance" android:excludeFromRecents="true" android:noHistory="true" diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java index 66aaa562b873..a01df8bf108d 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java @@ -37,6 +37,7 @@ import android.content.pm.PackageManagerInternal; import android.content.pm.Signature; import android.content.pm.SigningDetails; import android.content.pm.UserInfo; +import android.app.PropertyInvalidatedCache; import android.os.Build; import android.os.Handler; import android.os.Message; @@ -50,6 +51,8 @@ import android.util.SparseArray; import androidx.annotation.NonNull; +import android.app.ApplicationPackageManager; +import android.content.pm.PackageManager; import com.android.internal.pm.parsing.pkg.PackageImpl; import com.android.internal.pm.parsing.pkg.ParsedPackage; import com.android.internal.pm.pkg.component.ParsedActivity; @@ -64,8 +67,10 @@ import com.android.internal.pm.pkg.component.ParsedUsesPermissionImpl; import com.android.internal.pm.pkg.parsing.ParsingPackage; import com.android.server.om.OverlayReferenceMapper; import com.android.server.pm.pkg.AndroidPackage; +import com.android.server.utils.Watchable; import com.android.server.utils.WatchableTester; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -244,6 +249,55 @@ public class AppsFilterImplTest { (Answer<Boolean>) invocation -> ((AndroidPackage) invocation.getArgument(SYSTEM_USER)).getTargetSdkVersion() >= Build.VERSION_CODES.R); + PropertyInvalidatedCache.setTestMode(true); + PackageManager.sApplicationInfoCache.testPropertyName(); + ApplicationPackageManager.sGetPackagesForUidCache.testPropertyName(); + } + + @After + public void tearDown() { + PropertyInvalidatedCache.setTestMode(false); + } + + /** + * A class to make it easier to verify that PM caches are properly invalidated by + * AppsFilterImpl operations. This extends WatchableTester to test the cache nonces along + * with change reporting. + */ + private static class NonceTester extends WatchableTester { + // The nonces from caches under consideration. The no-parameter constructor fetches the + // values from the cacches. + private static record Nonces(long applicationInfo, long packageInfo) { + Nonces() { + this(ApplicationPackageManager.sGetPackagesForUidCache.getNonce(), + PackageManager.sApplicationInfoCache.getNonce()); + } + } + + // Track the latest cache nonces. + private Nonces mNonces; + + NonceTester(Watchable w, String k) { + super(w, k); + mNonces = new Nonces(); + } + + @Override + public void verifyChangeReported(String msg) { + super.verifyChangeReported(msg); + Nonces update = new Nonces(); + assertTrue(msg, update.applicationInfo != mNonces.applicationInfo); + assertTrue(msg, update.packageInfo != mNonces.packageInfo); + mNonces = update; + } + + @Override + public void verifyNoChangeReported(String msg) { + super.verifyNoChangeReported(msg); + Nonces update = new Nonces(); + assertTrue(msg, update.applicationInfo == mNonces.applicationInfo); + assertTrue(msg, update.packageInfo == mNonces.packageInfo); + } } @Test @@ -1167,7 +1221,7 @@ public class AppsFilterImplTest { final AppsFilterImpl appsFilter = new AppsFilterImpl(mFeatureConfigMock, new String[]{}, /* systemAppsQueryable */ false, /* overlayProvider */ null, mMockHandler); - final WatchableTester watcher = new WatchableTester(appsFilter, "onChange"); + final WatchableTester watcher = new NonceTester(appsFilter, "onChange"); watcher.register(); simulateAddBasicAndroid(appsFilter); watcher.verifyChangeReported("addBasicAndroid"); diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java index 5393e20889c0..b9cea0c72306 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java @@ -388,6 +388,34 @@ public class LocalDisplayAdapterTest { PORT_C, false); } + /** + * Confirm that display is marked as trusted, has own focus, disables steal top focus when it + * is listed in com.android.internal.R.array.config_localNotStealTopFocusDisplayPorts. + */ + @Test + public void testStealTopFocusDisabledDisplay() throws Exception { + setUpDisplay(new FakeDisplay(PORT_A)); + setUpDisplay(new FakeDisplay(PORT_B)); + setUpDisplay(new FakeDisplay(PORT_C)); + updateAvailableDisplays(); + + doReturn(new int[]{ PORT_B }).when(mMockedResources).getIntArray( + com.android.internal.R.array.config_localNotStealTopFocusDisplayPorts); + mAdapter.registerLocked(); + + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + + // This should not have the flags + assertNotStealTopFocusFlag(mListener.addedDisplays.get(0).getDisplayDeviceInfoLocked(), + PORT_A, false); + // This should have the flags + assertNotStealTopFocusFlag(mListener.addedDisplays.get(1).getDisplayDeviceInfoLocked(), + PORT_B, true); + // This should not have the flags + assertNotStealTopFocusFlag(mListener.addedDisplays.get(2).getDisplayDeviceInfoLocked(), + PORT_C, false); + } + @Test public void testSupportedDisplayModesGetOverriddenWhenDisplayIsUpdated() throws InterruptedException { @@ -452,6 +480,42 @@ public class LocalDisplayAdapterTest { } /** + * Confirm that all local displays are not trusted, do not have their own focus, and do not + * steal top focus when config_localNotStealTopFocusDisplayPorts is empty: + */ + @Test + public void testDisplayFlagsForNoConfigLocalNotStealTopFocusDisplayPorts() throws Exception { + setUpDisplay(new FakeDisplay(PORT_A)); + setUpDisplay(new FakeDisplay(PORT_C)); + updateAvailableDisplays(); + + // config_localNotStealTopFocusDisplayPorts is null + mAdapter.registerLocked(); + + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + + // This should not have the flags + assertNotStealTopFocusFlag(mListener.addedDisplays.get(0).getDisplayDeviceInfoLocked(), + PORT_A, false); + // This should not have the flags + assertNotStealTopFocusFlag(mListener.addedDisplays.get(1).getDisplayDeviceInfoLocked(), + PORT_C, false); + } + + private static void assertNotStealTopFocusFlag( + DisplayDeviceInfo info, int expectedPort, boolean shouldHaveFlags) { + final DisplayAddress.Physical address = (DisplayAddress.Physical) info.address; + assertNotNull(address); + assertEquals(expectedPort, address.getPort()); + assertEquals(DISPLAY_MODEL, address.getModel()); + assertEquals(shouldHaveFlags, + (info.flags & DisplayDeviceInfo.FLAG_STEAL_TOP_FOCUS_DISABLED) != 0); + assertEquals(shouldHaveFlags, (info.flags & DisplayDeviceInfo.FLAG_OWN_FOCUS) != 0); + // display is always trusted since it is created by the system + assertEquals(true, (info.flags & DisplayDeviceInfo.FLAG_TRUSTED) != 0); + } + + /** * Confirm that external display uses physical density. */ @Test diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java index f154dbcee21a..09ce263e9b2f 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java @@ -3962,7 +3962,7 @@ public class DisplayModeDirectorTest { } @Override - public VotesStatsReporter getVotesStatsReporter(boolean refreshRateVotingTelemetryEnabled) { + public VotesStatsReporter getVotesStatsReporter() { return null; } diff --git a/services/tests/mockingservicestests/AndroidManifest.xml b/services/tests/mockingservicestests/AndroidManifest.xml index aa3930ac7c07..b509b0f9fd92 100644 --- a/services/tests/mockingservicestests/AndroidManifest.xml +++ b/services/tests/mockingservicestests/AndroidManifest.xml @@ -52,6 +52,16 @@ <uses-library android:name="android.test.runner" /> <activity android:name="android.service.games.GameSessionTrampolineActivityTest$TestActivity" /> + <service android:name="com.android.server.wallpaper.TestWallpaperService" + android:label="Test Wallpaper Service" + android:exported="true" + android:permission="android.permission.BIND_WALLPAPER"> + <intent-filter> + <action android:name="android.service.wallpaper.WallpaperService"/> + </intent-filter> + <meta-data android:name="android.service.wallpaper" + android:resource="@xml/test_wallpaper"/> + </service> </application> <instrumentation diff --git a/services/tests/mockingservicestests/res/xml/test_wallpaper.xml b/services/tests/mockingservicestests/res/xml/test_wallpaper.xml new file mode 100644 index 000000000000..4eed477337b5 --- /dev/null +++ b/services/tests/mockingservicestests/res/xml/test_wallpaper.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 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. + --> +<wallpaper xmlns:android="http://schemas.android.com/apk/res/android" + android:label="Test Wallpaper" + android:supportsMultipleDisplays="true" /> diff --git a/services/tests/mockingservicestests/src/com/android/server/StorageManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/StorageManagerServiceTest.java index 2e4b97ef7dd2..371b0c926039 100644 --- a/services/tests/mockingservicestests/src/com/android/server/StorageManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/StorageManagerServiceTest.java @@ -26,6 +26,7 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.spy; +import android.app.PropertyInvalidatedCache; import android.content.Context; import android.multiuser.Flags; import android.os.UserManager; @@ -75,6 +76,8 @@ public class StorageManagerServiceTest { @Before public void setFixtures() { + PropertyInvalidatedCache.disableForTestMode(); + // Called when WatchedUserStates is constructed doNothing().when(() -> UserManager.invalidateIsUserUnlockedCache()); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java index 1e665c2c5c50..409706b14c56 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java @@ -1550,6 +1550,118 @@ public final class BroadcastQueueImplTest extends BaseBroadcastQueueTest { verifyPendingRecords(queue, List.of(closeSystemDialogs1, closeSystemDialogs2)); } + @SuppressWarnings("GuardedBy") + @Test + public void testDeliveryGroupPolicy_sameAction_multiplePolicies() { + // Create a PACKAGE_CHANGED broadcast corresponding to a change in the whole PACKAGE_GREEN + // package. + final Intent greenPackageChangedIntent = createPackageChangedIntent( + getUidForPackage(PACKAGE_GREEN), List.of(PACKAGE_GREEN)); + // Create delivery group policy such that when there are multiple broadcasts within the + // delivery group identified by "com.example.green/10002", only the most recent one + // gets delivered and the rest get discarded. + final BroadcastOptions optionsMostRecentPolicyForPackageGreen = + BroadcastOptions.makeBasic(); + optionsMostRecentPolicyForPackageGreen.setDeliveryGroupMatchingKey("package_changed", + PACKAGE_GREEN + "/" + getUidForPackage(PACKAGE_GREEN)); + optionsMostRecentPolicyForPackageGreen.setDeliveryGroupPolicy( + BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT); + + // Create a PACKAGE_CHANGED broadcast corresponding to a change in the whole PACKAGE_RED + // package. + final Intent redPackageChangedIntent = createPackageChangedIntent( + getUidForPackage(PACKAGE_RED), List.of(PACKAGE_RED)); + // Create delivery group policy such that when there are multiple broadcasts within the + // delivery group identified by "com.example.red/10001", only the most recent one + // gets delivered and the rest get discarded. + final BroadcastOptions optionsMostRecentPolicyForPackageRed = + BroadcastOptions.makeBasic(); + optionsMostRecentPolicyForPackageRed.setDeliveryGroupMatchingKey("package_changed", + PACKAGE_RED + "/" + getUidForPackage(PACKAGE_RED)); + optionsMostRecentPolicyForPackageRed.setDeliveryGroupPolicy( + BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT); + + // Create a PACKAGE_CHANGED broadcast corresponding to a change in some components of + // PACKAGE_GREEN package. + final Intent greenPackageComponentsChangedIntent1 = createPackageChangedIntent( + getUidForPackage(PACKAGE_GREEN), + List.of(PACKAGE_GREEN + ".comp1", PACKAGE_GREEN + ".comp2")); + final Intent greenPackageComponentsChangedIntent2 = createPackageChangedIntent( + getUidForPackage(PACKAGE_GREEN), + List.of(PACKAGE_GREEN + ".comp3")); + // Create delivery group policy such that when there are multiple broadcasts within the + // delivery group identified by "components-com.example.green/10002", merge the extras + // within these broadcasts such that only one broadcast is sent and the rest are + // discarded. Couple of things to note here: + // 1. We are intentionally using a different policy group + // "components-com.example.green/10002" (as opposed to "com.example.green/10002" used + // earlier), because this is corresponding to a change in some particular components, + // rather than a change to the whole package and we want to keep these two types of + // broadcasts independent. + // 2. We are using 'extrasMerger' to indicate how we want the extras to be merged. This + // assumes that broadcasts belonging to the group 'components-com.example.green/10002' + // will have the same values for all the extras, except for the one extra + // 'EXTRA_CHANGED_COMPONENT_NAME_LIST'. So, we explicitly specify how to merge this + // extra by using 'STRATEGY_ARRAY_APPEND' strategy, which basically indicates that + // the extra values which are arrays should be concatenated. + final BundleMerger extrasMerger = new BundleMerger(); + extrasMerger.setMergeStrategy(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, + BundleMerger.STRATEGY_ARRAY_APPEND); + final BroadcastOptions optionsMergedPolicyForPackageGreen = BroadcastOptions.makeBasic(); + optionsMergedPolicyForPackageGreen.setDeliveryGroupMatchingKey("package_changed", + "components-" + PACKAGE_GREEN + "/" + getUidForPackage(PACKAGE_GREEN)); + optionsMergedPolicyForPackageGreen.setDeliveryGroupPolicy( + BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED); + optionsMergedPolicyForPackageGreen.setDeliveryGroupExtrasMerger(extrasMerger); + + // Create a PACKAGE_CHANGED broadcast corresponding to a change in some components of + // PACKAGE_RED package. + final Intent redPackageComponentsChangedIntent = createPackageChangedIntent( + getUidForPackage(PACKAGE_RED), + List.of(PACKAGE_RED + ".comp1", PACKAGE_RED + ".comp2")); + // Create delivery group policy such that when there are multiple broadcasts within the + // delivery group identified by "components-com.example.red/10001", merge the extras + // within these broadcasts such that only one broadcast is sent and the rest are + // discarded. + final BroadcastOptions optionsMergedPolicyForPackageRed = BroadcastOptions.makeBasic(); + optionsMergedPolicyForPackageGreen.setDeliveryGroupMatchingKey("package_changed", + "components-" + PACKAGE_RED + "/" + getUidForPackage(PACKAGE_RED)); + optionsMergedPolicyForPackageRed.setDeliveryGroupPolicy( + BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED); + optionsMergedPolicyForPackageRed.setDeliveryGroupExtrasMerger(extrasMerger); + + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(greenPackageChangedIntent, + optionsMostRecentPolicyForPackageGreen)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(redPackageChangedIntent, + optionsMostRecentPolicyForPackageRed)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(greenPackageComponentsChangedIntent1, + optionsMergedPolicyForPackageGreen)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(redPackageComponentsChangedIntent, + optionsMergedPolicyForPackageRed)); + // Since this broadcast has DELIVERY_GROUP_MOST_RECENT policy set, the earlier + // greenPackageChangedIntent broadcast with the same policy will be discarded. + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(greenPackageChangedIntent, + optionsMostRecentPolicyForPackageGreen)); + // Since this broadcast has DELIVERY_GROUP_MERGED policy set, the earlier + // greenPackageComponentsChangedIntent1 broadcast with the same policy will be merged + // with this one and then will be discarded. + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(greenPackageComponentsChangedIntent2, + optionsMergedPolicyForPackageGreen)); + + final BroadcastProcessQueue queue = mImpl.getProcessQueue(PACKAGE_GREEN, + getUidForPackage(PACKAGE_GREEN)); + // The extra EXTRA_CHANGED_COMPONENT_NAME_LIST values from + // greenPackageComponentsChangedIntent1 and + // greenPackageComponentsChangedIntent2 broadcasts would be merged, since + // STRATEGY_ARRAY_APPEND was used for this extra. + final Intent expectedGreenPackageComponentsChangedIntent = createPackageChangedIntent( + getUidForPackage(PACKAGE_GREEN), List.of(PACKAGE_GREEN + ".comp3", + PACKAGE_GREEN + ".comp1", PACKAGE_GREEN + ".comp2")); + verifyPendingRecords(queue, List.of(redPackageChangedIntent, + redPackageComponentsChangedIntent, greenPackageChangedIntent, + expectedGreenPackageComponentsChangedIntent)); + } + private Pair<Intent, BroadcastOptions> createDropboxBroadcast(String tag, long timestampMs, int droppedCount) { final Intent dropboxEntryAdded = new Intent(DropBoxManager.ACTION_DROPBOX_ENTRY_ADDED); diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java index 86bf203771ba..409b114100e7 100644 --- a/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java @@ -27,6 +27,7 @@ import android.content.Context; import android.content.pm.PackageManager; import android.os.UserHandle; import android.os.UserManager; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; import android.util.ArraySet; @@ -73,6 +74,7 @@ public class SystemBackupAgentTest { } @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_BACKUP_AND_RESTORE_FOR_INPUT_GESTURES) public void onCreate_systemUser_addsAllHelpers() { UserHandle userHandle = new UserHandle(UserHandle.USER_SYSTEM); when(mUserManagerMock.isProfile()).thenReturn(false); @@ -94,10 +96,12 @@ public class SystemBackupAgentTest { "app_gender", "companion", "system_gender", - "display"); + "display", + "input"); } @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_BACKUP_AND_RESTORE_FOR_INPUT_GESTURES) public void onCreate_systemUser_slicesDisabled_addsAllNonSlicesHelpers() { UserHandle userHandle = new UserHandle(UserHandle.USER_SYSTEM); when(mUserManagerMock.isProfile()).thenReturn(false); @@ -120,10 +124,12 @@ public class SystemBackupAgentTest { "app_gender", "companion", "system_gender", - "display"); + "display", + "input"); } @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_BACKUP_AND_RESTORE_FOR_INPUT_GESTURES) public void onCreate_profileUser_addsProfileEligibleHelpers() { UserHandle userHandle = new UserHandle(NON_SYSTEM_USER_ID); when(mUserManagerMock.isProfile()).thenReturn(true); @@ -143,6 +149,7 @@ public class SystemBackupAgentTest { } @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_BACKUP_AND_RESTORE_FOR_INPUT_GESTURES) public void onCreate_nonSystemUser_addsNonSystemEligibleHelpers() { UserHandle userHandle = new UserHandle(NON_SYSTEM_USER_ID); when(mUserManagerMock.isProfile()).thenReturn(false); @@ -162,7 +169,8 @@ public class SystemBackupAgentTest { "companion", "app_gender", "system_gender", - "display"); + "display", + "input"); } @Test diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java index f79cb1105611..dd942edc7689 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java @@ -401,15 +401,27 @@ public final class UserManagerServiceTest { } @Test - public void testGetBootUser_Headless_ThrowsIfOnlySystemUserExists() throws Exception { + public void testGetBootUser_CannotSwitchToHeadlessSystemUser_ThrowsIfOnlySystemUserExists() + throws Exception { setSystemUserHeadless(true); removeNonSystemUsers(); + mockCanSwitchToHeadlessSystemUser(false); assertThrows(UserManager.CheckedUserOperationException.class, () -> mUmi.getBootUser(/* waitUntilSet= */ false)); } @Test + public void testGetBootUser_CanSwitchToHeadlessSystemUser_NoThrowIfOnlySystemUserExists() + throws Exception { + setSystemUserHeadless(true); + removeNonSystemUsers(); + mockCanSwitchToHeadlessSystemUser(true); + + assertThat(mUmi.getBootUser(/* waitUntilSet= */ false)).isEqualTo(UserHandle.USER_SYSTEM); + } + + @Test public void testGetPreviousFullUserToEnterForeground() throws Exception { addUser(USER_ID); setLastForegroundTime(USER_ID, 1_000_000L); diff --git a/core/java/com/android/internal/app/IAppOpsCallback.aidl b/services/tests/mockingservicestests/src/com/android/server/wallpaper/TestWallpaperService.java index 3a9525c03161..85ea5a0f2c2e 100644 --- a/core/java/com/android/internal/app/IAppOpsCallback.aidl +++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/TestWallpaperService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 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,10 +14,13 @@ * limitations under the License. */ -package com.android.internal.app; +package com.android.server.wallpaper; -// This interface is also used by native code, so must -// be kept in sync with frameworks/native/libs/permission/include/binder/IAppOpsCallback.h -oneway interface IAppOpsCallback { - void opChanged(int op, int uid, String packageName, String persistentDeviceId); +import android.service.wallpaper.WallpaperService; + +public final class TestWallpaperService extends WallpaperService { + @Override + public Engine onCreateEngine() { + return new Engine(); + } } diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java index a5073599b29e..bc04fd94c719 100644 --- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java @@ -136,6 +136,10 @@ public class WallpaperManagerServiceTests { private static final String TAG = "WallpaperManagerServiceTests"; private static final int DISPLAY_SIZE_DIMENSION = 100; + + private static final ComponentName TEST_WALLPAPER_COMPONENT = ComponentName.createRelative( + "com.android.frameworks.mockingservicestests", + "com.android.server.wallpaper.TestWallpaperService"); private static StaticMockitoSession sMockitoSession; @ClassRule @@ -144,6 +148,7 @@ public class WallpaperManagerServiceTests { private static ComponentName sImageWallpaperComponentName; private static ComponentName sDefaultWallpaperComponent; + private static WallpaperDescription sDefaultWallpaperDescription; private static ComponentName sFallbackWallpaperComponentName; @@ -210,8 +215,11 @@ public class WallpaperManagerServiceTests { } else { sContext.addMockService(sDefaultWallpaperComponent, sWallpaperService); } + sDefaultWallpaperDescription = new WallpaperDescription.Builder().setComponent( + sDefaultWallpaperComponent).build(); sContext.addMockService(sImageWallpaperComponentName, sWallpaperService); + sContext.addMockService(TEST_WALLPAPER_COMPONENT, sWallpaperService); if (sFallbackWallpaperComponentName != null) { sContext.addMockService(sFallbackWallpaperComponentName, sWallpaperService); } @@ -484,11 +492,12 @@ public class WallpaperManagerServiceTests { } @Test - @EnableFlags(Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT) + @EnableFlags({Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT, + Flags.FLAG_LIVE_WALLPAPER_CONTENT_HANDLING}) public void testSaveLoadSettings_withoutWallpaperDescription() throws IOException, XmlPullParserException { WallpaperData expectedData = mService.getCurrentWallpaperData(FLAG_SYSTEM, 0); - expectedData.setComponent(sDefaultWallpaperComponent); + expectedData.setDescription(sDefaultWallpaperDescription); expectedData.primaryColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); expectedData.mWallpaperDimAmount = 0.5f; @@ -524,11 +533,12 @@ public class WallpaperManagerServiceTests { } @Test - @EnableFlags(Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT) + @EnableFlags({Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT, + Flags.FLAG_LIVE_WALLPAPER_CONTENT_HANDLING}) public void testSaveLoadSettings_withWallpaperDescription() throws IOException, XmlPullParserException { WallpaperData expectedData = mService.getCurrentWallpaperData(FLAG_SYSTEM, 0); - expectedData.setComponent(sDefaultWallpaperComponent); + expectedData.setDescription(sDefaultWallpaperDescription); PersistableBundle content = new PersistableBundle(); content.putString("ckey", "cvalue"); WallpaperDescription description = new WallpaperDescription.Builder() @@ -556,7 +566,8 @@ public class WallpaperManagerServiceTests { } @Test - @DisableFlags(Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT) + @DisableFlags({Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT, + Flags.FLAG_LIVE_WALLPAPER_CONTENT_HANDLING}) public void testSaveLoadSettings_legacyNextComponent() throws IOException, XmlPullParserException { WallpaperData systemWallpaperData = mService.getCurrentWallpaperData(FLAG_SYSTEM, 0); @@ -1033,35 +1044,33 @@ public class WallpaperManagerServiceTests { } // Verify a secondary display removes system decorations ended - // Test setWallpaperComponent on multiple displays. - // GIVEN 3 displays, 0, 2, 3, the new wallpaper is only compatible for display 0 and 3 but not - // 2. - // WHEN the new wallpaper is set for system and lock via setWallpaperComponent. + // Test fallback connection is correctly established for multiple displays after reboot. + // GIVEN 3 displays, 0, 2, 3, the wallpaper is only compatible for display 0 and 3 but not 2. + // WHEN the device is booted. // THEN there are 2 connections in mLastWallpaper and 1 connection in mFallbackWallpaper. @Test @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER) - public void setWallpaperComponent_multiDisplays_shouldHaveExpectedConnections() { - // Skip if there is no pre-defined default wallpaper component. - assumeThat(sDefaultWallpaperComponent, - not(CoreMatchers.equalTo(sImageWallpaperComponentName))); - - final int testUserId = USER_SYSTEM; - mService.switchUser(testUserId, null); + public void deviceBooted_multiDisplays_shouldHaveExpectedConnections() { final int incompatibleDisplayId = 2; final int compatibleDisplayId = 3; setUpDisplays(List.of(DEFAULT_DISPLAY, incompatibleDisplayId, compatibleDisplayId)); mService.removeWallpaperCompatibleDisplayForTest(incompatibleDisplayId); - mService.setWallpaperComponent(sImageWallpaperComponentName, sContext.getOpPackageName(), - FLAG_SYSTEM | FLAG_LOCK, testUserId); + final int testUserId = USER_SYSTEM; + // After reboot, a switch user triggers the wallpapers initialization. + mService.switchUser(testUserId, null); verifyLastWallpaperData(testUserId, sImageWallpaperComponentName); verifyCurrentSystemData(testUserId); - assertThat(mService.mLastWallpaper.connection.getConnectedEngineSize()).isEqualTo(2); assertThat(mService.mLastWallpaper.connection.containsDisplay(DEFAULT_DISPLAY)).isTrue(); assertThat(mService.mLastWallpaper.connection.containsDisplay(compatibleDisplayId)) .isTrue(); - assertThat(mService.mFallbackWallpaper.connection.getConnectedEngineSize()).isEqualTo(1); + assertThat(mService.mLastWallpaper.connection.containsDisplay(incompatibleDisplayId)) + .isFalse(); + assertThat(mService.mFallbackWallpaper.connection.containsDisplay(DEFAULT_DISPLAY)) + .isFalse(); + assertThat(mService.mFallbackWallpaper.connection.containsDisplay(compatibleDisplayId)) + .isFalse(); assertThat(mService.mFallbackWallpaper.connection.containsDisplay(incompatibleDisplayId)) .isTrue(); assertThat(mService.mLastLockWallpaper).isNull(); @@ -1076,30 +1085,31 @@ public class WallpaperManagerServiceTests { @Test @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER) public void setWallpaperComponent_multiDisplays_displayBecomeCompatible_shouldHaveExpectedConnections() { - // Skip if there is no pre-defined default wallpaper component. - assumeThat(sDefaultWallpaperComponent, - not(CoreMatchers.equalTo(sImageWallpaperComponentName))); - - final int testUserId = USER_SYSTEM; - mService.switchUser(testUserId, null); final int display2 = 2; final int display3 = 3; setUpDisplays(List.of(DEFAULT_DISPLAY, display2, display3)); mService.removeWallpaperCompatibleDisplayForTest(display2); - mService.setWallpaperComponent(sImageWallpaperComponentName, sContext.getOpPackageName(), + final int testUserId = USER_SYSTEM; + mService.switchUser(testUserId, null); + // Switch to a test wallpaper and then image wallpaper later to simulate a wallpaper change. + mService.setWallpaperComponent(TEST_WALLPAPER_COMPONENT, sContext.getOpPackageName(), FLAG_SYSTEM | FLAG_LOCK, testUserId); - mService.addWallpaperCompatibleDisplayForTest(display2); + mService.setWallpaperComponent(sImageWallpaperComponentName, sContext.getOpPackageName(), FLAG_SYSTEM | FLAG_LOCK, testUserId); verifyLastWallpaperData(testUserId, sImageWallpaperComponentName); verifyCurrentSystemData(testUserId); - assertThat(mService.mLastWallpaper.connection.getConnectedEngineSize()).isEqualTo(3); assertThat(mService.mLastWallpaper.connection.containsDisplay(DEFAULT_DISPLAY)).isTrue(); assertThat(mService.mLastWallpaper.connection.containsDisplay(display2)).isTrue(); assertThat(mService.mLastWallpaper.connection.containsDisplay(display3)).isTrue(); - assertThat(mService.mFallbackWallpaper.connection.getConnectedEngineSize()).isEqualTo(0); + assertThat( + mService.mFallbackWallpaper.connection.containsDisplay(DEFAULT_DISPLAY)).isFalse(); + assertThat( + mService.mFallbackWallpaper.connection.containsDisplay(display2)).isFalse(); + assertThat( + mService.mFallbackWallpaper.connection.containsDisplay(display3)).isFalse(); assertThat(mService.mLastLockWallpaper).isNull(); } @@ -1112,28 +1122,27 @@ public class WallpaperManagerServiceTests { @Test @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER) public void setWallpaperComponent_multiDisplays_displayBecomeIncompatible_shouldHaveExpectedConnections() { - // Skip if there is no pre-defined default wallpaper component. - assumeThat(sDefaultWallpaperComponent, - not(CoreMatchers.equalTo(sImageWallpaperComponentName))); - - final int testUserId = USER_SYSTEM; - mService.switchUser(testUserId, null); final int display2 = 2; final int display3 = 3; setUpDisplays(List.of(DEFAULT_DISPLAY, display2, display3)); mService.removeWallpaperCompatibleDisplayForTest(display2); - mService.setWallpaperComponent(sImageWallpaperComponentName, sContext.getOpPackageName(), + final int testUserId = USER_SYSTEM; + mService.switchUser(testUserId, null); + // Switch to a test wallpaper and then image wallpaper later to simulate a wallpaper change. + mService.setWallpaperComponent(TEST_WALLPAPER_COMPONENT, sContext.getOpPackageName(), FLAG_SYSTEM | FLAG_LOCK, testUserId); - mService.removeWallpaperCompatibleDisplayForTest(display3); + mService.setWallpaperComponent(sImageWallpaperComponentName, sContext.getOpPackageName(), FLAG_SYSTEM | FLAG_LOCK, testUserId); verifyLastWallpaperData(testUserId, sImageWallpaperComponentName); verifyCurrentSystemData(testUserId); - assertThat(mService.mLastWallpaper.connection.getConnectedEngineSize()).isEqualTo(1); assertThat(mService.mLastWallpaper.connection.containsDisplay(DEFAULT_DISPLAY)).isTrue(); - assertThat(mService.mFallbackWallpaper.connection.getConnectedEngineSize()).isEqualTo(2); + assertThat(mService.mLastWallpaper.connection.containsDisplay(display2)).isFalse(); + assertThat(mService.mLastWallpaper.connection.containsDisplay(display3)).isFalse(); + assertThat( + mService.mFallbackWallpaper.connection.containsDisplay(DEFAULT_DISPLAY)).isFalse(); assertThat(mService.mFallbackWallpaper.connection.containsDisplay(display2)).isTrue(); assertThat(mService.mFallbackWallpaper.connection.containsDisplay(display3)).isTrue(); assertThat(mService.mLastLockWallpaper).isNull(); @@ -1148,35 +1157,40 @@ public class WallpaperManagerServiceTests { @Test @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER) public void setWallpaperComponent_systemAndLockWallpapers_multiDisplays_shouldHaveExpectedConnections() { - // Skip if there is no pre-defined default wallpaper component. - assumeThat(sDefaultWallpaperComponent, - not(CoreMatchers.equalTo(sImageWallpaperComponentName))); - - final int testUserId = USER_SYSTEM; - mService.switchUser(testUserId, null); final int incompatibleDisplayId = 2; final int compatibleDisplayId = 3; setUpDisplays(List.of(DEFAULT_DISPLAY, incompatibleDisplayId, compatibleDisplayId)); + final int testUserId = USER_SYSTEM; + mService.switchUser(testUserId, null); + // Switch to a test wallpaper and then image wallpaper later to simulate a wallpaper change. + mService.setWallpaperComponent(TEST_WALLPAPER_COMPONENT, sContext.getOpPackageName(), + FLAG_SYSTEM | FLAG_LOCK, testUserId); mService.removeWallpaperCompatibleDisplayForTest(incompatibleDisplayId); mService.setWallpaperComponent(sImageWallpaperComponentName, sContext.getOpPackageName(), FLAG_SYSTEM, testUserId); - mService.setWallpaperComponent(sImageWallpaperComponentName, sContext.getOpPackageName(), - FLAG_LOCK, testUserId); verifyLastWallpaperData(testUserId, sImageWallpaperComponentName); - verifyLastLockWallpaperData(testUserId, sImageWallpaperComponentName); + verifyLastLockWallpaperData(testUserId, TEST_WALLPAPER_COMPONENT); verifyCurrentSystemData(testUserId); - assertThat(mService.mLastWallpaper.connection.getConnectedEngineSize()).isEqualTo(2); + assertThat(mService.mLastWallpaper.connection.containsDisplay(DEFAULT_DISPLAY)).isTrue(); assertThat(mService.mLastWallpaper.connection.containsDisplay(compatibleDisplayId)) .isTrue(); - assertThat(mService.mLastLockWallpaper.connection.getConnectedEngineSize()).isEqualTo(2); + assertThat(mService.mLastWallpaper.connection.containsDisplay(incompatibleDisplayId)) + .isFalse(); + // mLastLockWallpaper is TEST_WALLPAPER_COMPONENT, which declares external displays support + // in the wallpaper metadata. assertThat(mService.mLastLockWallpaper.connection.containsDisplay(DEFAULT_DISPLAY)) .isTrue(); assertThat(mService.mLastLockWallpaper.connection.containsDisplay(compatibleDisplayId)) .isTrue(); - assertThat(mService.mFallbackWallpaper.connection.getConnectedEngineSize()).isEqualTo(1); + assertThat(mService.mLastLockWallpaper.connection.containsDisplay(incompatibleDisplayId)) + .isTrue(); + assertThat(mService.mFallbackWallpaper.connection.containsDisplay(DEFAULT_DISPLAY)) + .isFalse(); + assertThat(mService.mFallbackWallpaper.connection.containsDisplay(compatibleDisplayId)) + .isFalse(); assertThat(mService.mFallbackWallpaper.connection.containsDisplay(incompatibleDisplayId)) .isTrue(); } @@ -1281,4 +1295,6 @@ public class WallpaperManagerServiceTests { assertEquals(pfdContents, fileContents); } } + + } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/OWNERS b/services/tests/servicestests/src/com/android/server/accessibility/OWNERS index c824c3948e2d..c7c23f081044 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/OWNERS +++ b/services/tests/servicestests/src/com/android/server/accessibility/OWNERS @@ -1,3 +1,6 @@ -# Bug component: 44215 +# Bug component: 1530954 +# +# The above component is for automated test bugs. If you are a human looking to report +# a bug in this codebase then please use component 44215. include /core/java/android/view/accessibility/OWNERS diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java index 457fde8d74d0..0227ef1d2dc0 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java @@ -85,7 +85,7 @@ public class AutoclickControllerTest { public void onMotionEvent_lazyInitClickScheduler() { assertThat(mController.mClickScheduler).isNull(); - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); assertThat(mController.mClickScheduler).isNotNull(); } @@ -94,7 +94,7 @@ public class AutoclickControllerTest { public void onMotionEvent_nonMouseSource_notInitClickScheduler() { assertThat(mController.mClickScheduler).isNull(); - injectFakeNonMouseActionDownEvent(); + injectFakeNonMouseActionHoverMoveEvent(); assertThat(mController.mClickScheduler).isNull(); } @@ -103,7 +103,7 @@ public class AutoclickControllerTest { public void onMotionEvent_lazyInitAutoclickSettingsObserver() { assertThat(mController.mAutoclickSettingsObserver).isNull(); - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); assertThat(mController.mAutoclickSettingsObserver).isNotNull(); } @@ -113,7 +113,7 @@ public class AutoclickControllerTest { public void onMotionEvent_flagOn_lazyInitAutoclickIndicatorScheduler() { assertThat(mController.mAutoclickIndicatorScheduler).isNull(); - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); assertThat(mController.mAutoclickIndicatorScheduler).isNotNull(); } @@ -123,7 +123,7 @@ public class AutoclickControllerTest { public void onMotionEvent_flagOff_notInitAutoclickIndicatorScheduler() { assertThat(mController.mAutoclickIndicatorScheduler).isNull(); - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); assertThat(mController.mAutoclickIndicatorScheduler).isNull(); } @@ -133,7 +133,7 @@ public class AutoclickControllerTest { public void onMotionEvent_flagOn_lazyInitAutoclickIndicatorView() { assertThat(mController.mAutoclickIndicatorView).isNull(); - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); assertThat(mController.mAutoclickIndicatorView).isNotNull(); } @@ -143,7 +143,7 @@ public class AutoclickControllerTest { public void onMotionEvent_flagOff_notInitAutoclickIndicatorView() { assertThat(mController.mAutoclickIndicatorView).isNull(); - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); assertThat(mController.mAutoclickIndicatorView).isNull(); } @@ -153,7 +153,7 @@ public class AutoclickControllerTest { public void onMotionEvent_flagOn_lazyInitAutoclickTypePanelView() { assertThat(mController.mAutoclickTypePanel).isNull(); - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); assertThat(mController.mAutoclickTypePanel).isNotNull(); } @@ -163,7 +163,7 @@ public class AutoclickControllerTest { public void onMotionEvent_flagOff_notInitAutoclickTypePanelView() { assertThat(mController.mAutoclickTypePanel).isNull(); - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); assertThat(mController.mAutoclickTypePanel).isNull(); } @@ -171,7 +171,7 @@ public class AutoclickControllerTest { @Test @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) public void onMotionEvent_flagOn_addAutoclickIndicatorViewToWindowManager() { - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); verify(mMockWindowManager).addView(eq(mController.mAutoclickIndicatorView), any()); } @@ -179,7 +179,7 @@ public class AutoclickControllerTest { @Test @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) public void onDestroy_flagOn_removeAutoclickIndicatorViewToWindowManager() { - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); mController.onDestroy(); @@ -189,7 +189,7 @@ public class AutoclickControllerTest { @Test @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) public void onDestroy_flagOn_removeAutoclickTypePanelViewToWindowManager() { - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); AutoclickTypePanel mockAutoclickTypePanel = mock(AutoclickTypePanel.class); mController.mAutoclickTypePanel = mockAutoclickTypePanel; @@ -200,7 +200,7 @@ public class AutoclickControllerTest { @Test public void onMotionEvent_initClickSchedulerDelayFromSetting() { - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); int delay = Settings.Secure.getIntForUser( @@ -214,7 +214,7 @@ public class AutoclickControllerTest { @Test @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) public void onMotionEvent_flagOn_initCursorAreaSizeFromSetting() { - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); int size = Settings.Secure.getIntForUser( @@ -238,7 +238,7 @@ public class AutoclickControllerTest { @Test @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) public void onKeyEvent_modifierKey_updateMetaStateWhenControllerNotNull() { - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); int metaState = KeyEvent.META_ALT_ON | KeyEvent.META_META_ON; injectFakeKeyEvent(KeyEvent.KEYCODE_ALT_LEFT, metaState); @@ -250,7 +250,7 @@ public class AutoclickControllerTest { @Test @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) public void onKeyEvent_modifierKey_cancelAutoClickWhenAdditionalRegularKeyPresssed() { - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); injectFakeKeyEvent(KeyEvent.KEYCODE_J, KeyEvent.META_ALT_ON); @@ -260,7 +260,7 @@ public class AutoclickControllerTest { @Test public void onDestroy_clearClickScheduler() { - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); mController.onDestroy(); @@ -269,7 +269,7 @@ public class AutoclickControllerTest { @Test public void onDestroy_clearAutoclickSettingsObserver() { - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); mController.onDestroy(); @@ -279,21 +279,61 @@ public class AutoclickControllerTest { @Test @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) public void onDestroy_flagOn_clearAutoclickIndicatorScheduler() { - injectFakeMouseActionDownEvent(); + injectFakeMouseActionHoverMoveEvent(); mController.onDestroy(); assertThat(mController.mAutoclickIndicatorScheduler).isNull(); } - private void injectFakeMouseActionDownEvent() { - MotionEvent event = getFakeMotionDownEvent(); + @Test + @DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) + public void onMotionEvent_hoverEnter_doesNotScheduleClick() { + injectFakeMouseActionHoverMoveEvent(); + + // Send hover enter event. + MotionEvent hoverEnter = MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 100, + /* action= */ MotionEvent.ACTION_HOVER_ENTER, + /* x= */ 30f, + /* y= */ 0f, + /* metaState= */ 0); + hoverEnter.setSource(InputDevice.SOURCE_MOUSE); + mController.onMotionEvent(hoverEnter, hoverEnter, /* policyFlags= */ 0); + + // Verify there is no pending click. + assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse(); + } + + @Test + @DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) + public void onMotionEvent_hoverMove_scheduleClick() { + injectFakeMouseActionHoverMoveEvent(); + + // Send hover move event. + MotionEvent hoverMove = MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 100, + /* action= */ MotionEvent.ACTION_HOVER_MOVE, + /* x= */ 30f, + /* y= */ 0f, + /* metaState= */ 0); + hoverMove.setSource(InputDevice.SOURCE_MOUSE); + mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + + // Verify there is a pending click. + assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue(); + } + + private void injectFakeMouseActionHoverMoveEvent() { + MotionEvent event = getFakeMotionHoverMoveEvent(); event.setSource(InputDevice.SOURCE_MOUSE); mController.onMotionEvent(event, event, /* policyFlags= */ 0); } - private void injectFakeNonMouseActionDownEvent() { - MotionEvent event = getFakeMotionDownEvent(); + private void injectFakeNonMouseActionHoverMoveEvent() { + MotionEvent event = getFakeMotionHoverMoveEvent(); event.setSource(InputDevice.SOURCE_KEYBOARD); mController.onMotionEvent(event, event, /* policyFlags= */ 0); } @@ -309,11 +349,11 @@ public class AutoclickControllerTest { mController.onKeyEvent(keyEvent, /* policyFlags= */ 0); } - private MotionEvent getFakeMotionDownEvent() { + private MotionEvent getFakeMotionHoverMoveEvent() { return MotionEvent.obtain( /* downTime= */ 0, /* eventTime= */ 0, - /* action= */ MotionEvent.ACTION_DOWN, + /* action= */ MotionEvent.ACTION_HOVER_MOVE, /* x= */ 0, /* y= */ 0, /* metaState= */ 0); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java index f0334598bd30..7b8824cb0e3d 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java @@ -21,6 +21,7 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat import static com.google.common.truth.Truth.assertThat; import android.content.Context; +import android.graphics.drawable.GradientDrawable; import android.testing.AndroidTestingRunner; import android.testing.TestableContext; import android.testing.TestableLooper; @@ -28,6 +29,8 @@ import android.view.View; import android.view.WindowManager; import android.widget.LinearLayout; +import androidx.annotation.NonNull; + import com.android.internal.R; import org.junit.Before; @@ -87,6 +90,11 @@ public class AutoclickTypePanelTest { } @Test + public void AutoclickTypePanel_initialState_correctButtonStyle() { + verifyButtonHasSelectedStyle(mLeftClickButton); + } + + @Test public void togglePanelExpansion_onClick_expandedTrue() { // On clicking left click button, the panel is expanded and all buttons are visible. mLeftClickButton.callOnClick(); @@ -116,4 +124,21 @@ public class AutoclickTypePanelTest { assertThat(mDoubleClickButton.getVisibility()).isEqualTo(View.GONE); assertThat(mDragButton.getVisibility()).isEqualTo(View.GONE); } + + @Test + public void togglePanelExpansion_selectButton_correctStyle() { + // By first click, the panel is expanded. + mLeftClickButton.callOnClick(); + + // Clicks any button in the expanded state to select a type button. + mScrollButton.callOnClick(); + + verifyButtonHasSelectedStyle(mScrollButton); + } + + private void verifyButtonHasSelectedStyle(@NonNull LinearLayout button) { + GradientDrawable gradientDrawable = (GradientDrawable) button.getBackground(); + assertThat(gradientDrawable.getColor().getDefaultColor()) + .isEqualTo(mTestableContext.getColor(R.color.materialColorPrimary)); + } } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/OWNERS b/services/tests/servicestests/src/com/android/server/accessibility/magnification/OWNERS new file mode 100644 index 000000000000..9592bfdfa73b --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/OWNERS @@ -0,0 +1,6 @@ +# Bug component: 1530954 +# +# The above component is for automated test bugs. If you are a human looking to report +# a bug in this codebase then please use component 770744. + +include /services/accessibility/java/com/android/server/accessibility/magnification/OWNERS diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java index 1627f683cd3e..06958b81d846 100644 --- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java @@ -25,7 +25,6 @@ import static android.app.ActivityManagerInternal.ALLOW_FULL_ONLY; import static android.app.ActivityManagerInternal.ALLOW_NON_FULL; import static android.app.ActivityManagerInternal.ALLOW_NON_FULL_IN_PROFILE; import static android.app.ActivityManagerInternal.ALLOW_PROFILES_OR_NON_FULL; -import static android.app.KeyguardManager.LOCK_ON_USER_SWITCH_CALLBACK; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.testing.DexmakerShareClassLoaderRule.runWithDexmakerShareClassLoader; @@ -116,6 +115,7 @@ import com.android.server.pm.UserManagerInternal; import com.android.server.pm.UserManagerService; import com.android.server.pm.UserTypeDetails; import com.android.server.pm.UserTypeFactory; +import com.android.server.wm.ActivityTaskManagerInternal; import com.android.server.wm.WindowManagerService; import com.google.common.collect.Range; @@ -1563,11 +1563,11 @@ public class UserControllerTest { // and the thread is still alive assertTrue(threadStartUser.isAlive()); - // mock the binder response for the user switch completion - ArgumentCaptor<Bundle> captor = ArgumentCaptor.forClass(Bundle.class); - verify(mInjector.mWindowManagerMock).lockNow(captor.capture()); - IRemoteCallback.Stub.asInterface(captor.getValue().getBinder( - LOCK_ON_USER_SWITCH_CALLBACK)).sendResult(null); + // mock send the keyguard shown event + ArgumentCaptor<ActivityTaskManagerInternal.ScreenObserver> captor = ArgumentCaptor.forClass( + ActivityTaskManagerInternal.ScreenObserver.class); + verify(mInjector.mActivityTaskManagerInternal).registerScreenObserver(captor.capture()); + captor.getValue().onKeyguardStateChanged(true); // verify the switch now moves on... Thread.sleep(1000); @@ -1757,6 +1757,7 @@ public class UserControllerTest { private final IStorageManager mStorageManagerMock; private final UserManagerInternal mUserManagerInternalMock; private final WindowManagerService mWindowManagerMock; + private final ActivityTaskManagerInternal mActivityTaskManagerInternal; private final PowerManagerInternal mPowerManagerInternal; private final AlarmManagerInternal mAlarmManagerInternal; private final KeyguardManager mKeyguardManagerMock; @@ -1778,6 +1779,7 @@ public class UserControllerTest { mUserManagerMock = mock(UserManagerService.class); mUserManagerInternalMock = mock(UserManagerInternal.class); mWindowManagerMock = mock(WindowManagerService.class); + mActivityTaskManagerInternal = mock(ActivityTaskManagerInternal.class); mStorageManagerMock = mock(IStorageManager.class); mPowerManagerInternal = mock(PowerManagerInternal.class); mAlarmManagerInternal = mock(AlarmManagerInternal.class); @@ -1841,6 +1843,11 @@ public class UserControllerTest { } @Override + ActivityTaskManagerInternal getActivityTaskManagerInternal() { + return mActivityTaskManagerInternal; + } + + @Override PowerManagerInternal getPowerManagerInternal() { return mPowerManagerInternal; } diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index ffcb96120b19..ab7b4da269db 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -73,6 +73,7 @@ import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.hardware.Sensor; +import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManagerGlobal; import android.hardware.display.DisplayManagerInternal; import android.hardware.display.IDisplayManager; @@ -173,8 +174,7 @@ public class VirtualDeviceManagerServiceTest { private static final int FLAG_CANNOT_DISPLAY_ON_REMOTE_DEVICES = 0x00000; private static final int VIRTUAL_DEVICE_ID_1 = 42; private static final int VIRTUAL_DEVICE_ID_2 = 43; - private static final VirtualDisplayConfig VIRTUAL_DISPLAY_CONFIG = - new VirtualDisplayConfig.Builder("virtual_display", 640, 480, 400).build(); + private static final VirtualDpadConfig DPAD_CONFIG = new VirtualDpadConfig.Builder() .setVendorId(VENDOR_ID) @@ -284,7 +284,12 @@ public class VirtualDeviceManagerServiceTest { private Intent createRestrictedActivityBlockedIntent(Set<String> displayCategories, String targetDisplayCategory) { when(mDisplayManagerInternalMock.createVirtualDisplay(any(), any(), any(), any(), - eq(VIRTUAL_DEVICE_OWNER_PACKAGE))).thenReturn(DISPLAY_ID_1); + eq(VIRTUAL_DEVICE_OWNER_PACKAGE))) + .thenAnswer(inv -> { + mLocalService.onVirtualDisplayCreated( + mDeviceImpl, DISPLAY_ID_1, inv.getArgument(1), inv.getArgument(3)); + return DISPLAY_ID_1; + }); VirtualDisplayConfig config = new VirtualDisplayConfig.Builder("display", 640, 480, 420).setDisplayCategories(displayCategories).build(); mDeviceImpl.createVirtualDisplay(config, mVirtualDisplayCallback); @@ -997,8 +1002,7 @@ public class VirtualDeviceManagerServiceTest { public void onVirtualDisplayCreatedLocked_duplicateCalls_onlyOneWakeLockIsAcquired() throws RemoteException { addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED); - assertThrows(IllegalStateException.class, - () -> addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1)); + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED); TestableLooper.get(this).processAllMessages(); verify(mIPowerManagerMock).acquireWakeLock(any(Binder.class), anyInt(), nullable(String.class), nullable(String.class), nullable(WorkSource.class), @@ -1871,8 +1875,6 @@ public class VirtualDeviceManagerServiceTest { } private void addVirtualDisplay(VirtualDeviceImpl virtualDevice, int displayId, int flags) { - when(mDisplayManagerInternalMock.createVirtualDisplay(any(), eq(mVirtualDisplayCallback), - eq(virtualDevice), any(), any())).thenReturn(displayId); final String uniqueId = UNIQUE_ID + displayId; doAnswer(inv -> { final DisplayInfo displayInfo = new DisplayInfo(); @@ -1880,7 +1882,22 @@ public class VirtualDeviceManagerServiceTest { displayInfo.flags = flags; return displayInfo; }).when(mDisplayManagerInternalMock).getDisplayInfo(eq(displayId)); - virtualDevice.createVirtualDisplay(VIRTUAL_DISPLAY_CONFIG, mVirtualDisplayCallback); + + when(mDisplayManagerInternalMock.createVirtualDisplay(any(), eq(mVirtualDisplayCallback), + eq(virtualDevice), any(), any())).thenAnswer(inv -> { + mLocalService.onVirtualDisplayCreated( + virtualDevice, displayId, mVirtualDisplayCallback, inv.getArgument(3)); + return displayId; + }); + + final int virtualDisplayFlags = (flags & Display.FLAG_TRUSTED) == 0 + ? 0 + : DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED; + VirtualDisplayConfig virtualDisplayConfig = + new VirtualDisplayConfig.Builder("virtual_display", 640, 480, 400) + .setFlags(virtualDisplayFlags) + .build(); + virtualDevice.createVirtualDisplay(virtualDisplayConfig, mVirtualDisplayCallback); mInputManagerMockHelper.addDisplayIdMapping(uniqueId, displayId); } diff --git a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java index 04d075211297..a4e77c00d647 100644 --- a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java +++ b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java @@ -20,10 +20,12 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; +import android.hardware.contexthub.EndpointInfo; import android.hardware.contexthub.ErrorCode; import android.hardware.contexthub.HubEndpointInfo; import android.hardware.contexthub.HubEndpointInfo.HubEndpointIdentifier; @@ -32,6 +34,7 @@ import android.hardware.contexthub.IContextHubEndpoint; import android.hardware.contexthub.IContextHubEndpointCallback; import android.hardware.contexthub.IEndpointCommunication; import android.hardware.contexthub.MessageDeliveryStatus; +import android.hardware.contexthub.Reason; import android.os.Binder; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; @@ -61,6 +64,9 @@ public class ContextHubEndpointTest { private static final int ENDPOINT_ID = 1; private static final String ENDPOINT_PACKAGE_NAME = "com.android.server.location.contexthub"; + private static final String TARGET_ENDPOINT_NAME = "Example target endpoint"; + private static final int TARGET_ENDPOINT_ID = 1; + private ContextHubClientManager mClientManager; private ContextHubEndpointManager mEndpointManager; private HubInfoRegistry mHubInfoRegistry; @@ -95,23 +101,8 @@ public class ContextHubEndpointTest { @Test public void testRegisterEndpoint() throws RemoteException { - // Register an endpoint and confirm we can get a valid IContextHubEndoint reference - HubEndpointInfo info = - new HubEndpointInfo( - ENDPOINT_NAME, ENDPOINT_ID, ENDPOINT_PACKAGE_NAME, Collections.emptyList()); - IContextHubEndpoint endpoint = - mEndpointManager.registerEndpoint( - info, mMockCallback, ENDPOINT_PACKAGE_NAME, /* attributionTag= */ null); - assertThat(mEndpointManager.getNumRegisteredClients()).isEqualTo(1); - assertThat(endpoint).isNotNull(); - HubEndpointInfo assignedInfo = endpoint.getAssignedHubEndpointInfo(); - assertThat(assignedInfo).isNotNull(); - HubEndpointIdentifier assignedIdentifier = assignedInfo.getIdentifier(); - assertThat(assignedIdentifier).isNotNull(); - - // Unregister the endpoint and confirm proper clean-up - mEndpointManager.unregisterEndpoint(assignedIdentifier.getEndpoint()); - assertThat(mEndpointManager.getNumRegisteredClients()).isEqualTo(0); + IContextHubEndpoint endpoint = registerExampleEndpoint(); + unregisterExampleEndpoint(endpoint); } @Test @@ -146,4 +137,107 @@ public class ContextHubEndpointTest { assertThat(statusCaptor.getValue().messageSequenceNumber).isEqualTo(sequenceNumber); assertThat(statusCaptor.getValue().errorCode).isEqualTo(ErrorCode.DESTINATION_NOT_FOUND); } + + @Test + public void testHalRestart() throws RemoteException { + IContextHubEndpoint endpoint = registerExampleEndpoint(); + + // Verify that the endpoint is still registered after a HAL restart + HubEndpointInfo assignedInfo = endpoint.getAssignedHubEndpointInfo(); + HubEndpointIdentifier assignedIdentifier = assignedInfo.getIdentifier(); + mEndpointManager.onHalRestart(); + ArgumentCaptor<EndpointInfo> statusCaptor = ArgumentCaptor.forClass(EndpointInfo.class); + verify(mMockEndpointCommunications, times(2)).registerEndpoint(statusCaptor.capture()); + assertThat(statusCaptor.getValue().id.id).isEqualTo(assignedIdentifier.getEndpoint()); + assertThat(statusCaptor.getValue().id.hubId).isEqualTo(assignedIdentifier.getHub()); + + unregisterExampleEndpoint(endpoint); + } + + @Test + public void testHalRestartOnOpenSession() throws RemoteException { + assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE); + IContextHubEndpoint endpoint = registerExampleEndpoint(); + + HubEndpointInfo targetInfo = + new HubEndpointInfo( + TARGET_ENDPOINT_NAME, + TARGET_ENDPOINT_ID, + ENDPOINT_PACKAGE_NAME, + Collections.emptyList()); + int sessionId = endpoint.openSession(targetInfo, /* serviceDescriptor= */ null); + mEndpointManager.onEndpointSessionOpenComplete(sessionId); + assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE - 1); + + mEndpointManager.onHalRestart(); + + HubEndpointInfo assignedInfo = endpoint.getAssignedHubEndpointInfo(); + HubEndpointIdentifier assignedIdentifier = assignedInfo.getIdentifier(); + ArgumentCaptor<EndpointInfo> statusCaptor = ArgumentCaptor.forClass(EndpointInfo.class); + verify(mMockEndpointCommunications, times(2)).registerEndpoint(statusCaptor.capture()); + assertThat(statusCaptor.getValue().id.id).isEqualTo(assignedIdentifier.getEndpoint()); + assertThat(statusCaptor.getValue().id.hubId).isEqualTo(assignedIdentifier.getHub()); + + verify(mMockCallback) + .onSessionClosed( + sessionId, ContextHubServiceUtil.toAppHubEndpointReason(Reason.HUB_RESET)); + + unregisterExampleEndpoint(endpoint); + assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE); + } + + @Test + public void testOpenSessionOnUnregistration() throws RemoteException { + assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE); + IContextHubEndpoint endpoint = registerExampleEndpoint(); + + HubEndpointInfo targetInfo = + new HubEndpointInfo( + TARGET_ENDPOINT_NAME, + TARGET_ENDPOINT_ID, + ENDPOINT_PACKAGE_NAME, + Collections.emptyList()); + int sessionId = endpoint.openSession(targetInfo, /* serviceDescriptor= */ null); + mEndpointManager.onEndpointSessionOpenComplete(sessionId); + assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE - 1); + + unregisterExampleEndpoint(endpoint); + verify(mMockEndpointCommunications).closeEndpointSession(sessionId, Reason.ENDPOINT_GONE); + assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE); + } + + private IContextHubEndpoint registerExampleEndpoint() throws RemoteException { + HubEndpointInfo info = + new HubEndpointInfo( + ENDPOINT_NAME, ENDPOINT_ID, ENDPOINT_PACKAGE_NAME, Collections.emptyList()); + IContextHubEndpoint endpoint = + mEndpointManager.registerEndpoint( + info, mMockCallback, ENDPOINT_PACKAGE_NAME, /* attributionTag= */ null); + assertThat(endpoint).isNotNull(); + HubEndpointInfo assignedInfo = endpoint.getAssignedHubEndpointInfo(); + assertThat(assignedInfo).isNotNull(); + HubEndpointIdentifier assignedIdentifier = assignedInfo.getIdentifier(); + assertThat(assignedIdentifier).isNotNull(); + + // Confirm registerEndpoint was called with the right contents + ArgumentCaptor<EndpointInfo> statusCaptor = ArgumentCaptor.forClass(EndpointInfo.class); + verify(mMockEndpointCommunications).registerEndpoint(statusCaptor.capture()); + assertThat(statusCaptor.getValue().id.id).isEqualTo(assignedIdentifier.getEndpoint()); + assertThat(statusCaptor.getValue().id.hubId).isEqualTo(assignedIdentifier.getHub()); + assertThat(mEndpointManager.getNumRegisteredClients()).isEqualTo(1); + + return endpoint; + } + + private void unregisterExampleEndpoint(IContextHubEndpoint endpoint) throws RemoteException { + HubEndpointInfo expectedInfo = endpoint.getAssignedHubEndpointInfo(); + endpoint.unregister(); + ArgumentCaptor<EndpointInfo> statusCaptor = ArgumentCaptor.forClass(EndpointInfo.class); + verify(mMockEndpointCommunications).unregisterEndpoint(statusCaptor.capture()); + assertThat(statusCaptor.getValue().id.id) + .isEqualTo(expectedInfo.getIdentifier().getEndpoint()); + assertThat(statusCaptor.getValue().id.hubId) + .isEqualTo(expectedInfo.getIdentifier().getHub()); + assertThat(mEndpointManager.getNumRegisteredClients()).isEqualTo(0); + } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java index 770712a191fd..41011928f8b3 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java @@ -204,7 +204,7 @@ public class NotificationChannelExtractorTest extends UiServiceTestCase { .build()); final Notification n = new Notification.Builder(getContext()) .setContentTitle("foo") - .setCategory(CATEGORY_ALARM) + .setCategory(new String("alarm")) .setSmallIcon(android.R.drawable.sym_def_app_icon) .build(); NotificationRecord r = getRecord(channel, n); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 65150e7b48fc..440f43e9b926 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -49,17 +49,13 @@ import static android.content.res.Configuration.UI_MODE_TYPE_DESK; import static android.os.InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS; import static android.os.Process.NOBODY_UID; import static android.view.Display.DEFAULT_DISPLAY; -import static android.view.InsetsSource.ID_IME; -import static android.view.WindowInsets.Type.ime; import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW; import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW; -import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; import static android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD; import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; -import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN; import static android.view.WindowManager.TRANSIT_PIP; @@ -125,7 +121,6 @@ import android.app.servertransaction.ClientTransaction; import android.app.servertransaction.ClientTransactionItem; import android.app.servertransaction.DestroyActivityItem; import android.app.servertransaction.PauseActivityItem; -import android.app.servertransaction.WindowStateResizeItem; import android.compat.testing.PlatformCompatChangeRule; import android.content.ComponentName; import android.content.Intent; @@ -149,8 +144,6 @@ import android.view.DisplayInfo; import android.view.IRemoteAnimationFinishedCallback; import android.view.IRemoteAnimationRunner.Stub; import android.view.IWindowManager; -import android.view.InsetsSource; -import android.view.InsetsState; import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationTarget; import android.view.Surface; @@ -171,7 +164,6 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; import org.mockito.invocation.InvocationOnMock; import java.util.ArrayList; @@ -3370,178 +3362,6 @@ public class ActivityRecordTests extends WindowTestsBase { assertFalse(activity.mDisplayContent.mClosingApps.contains(activity)); } - @SetupWindows(addWindows = W_INPUT_METHOD) - @Test - public void testImeInsetsFrozenFlag_resetWhenNoImeFocusableInActivity() { - final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); - makeWindowVisibleAndDrawn(app, mImeWindow); - mDisplayContent.setImeLayeringTarget(app); - mDisplayContent.setImeInputTarget(app); - - // Simulate app is closing and expect the last IME is shown and IME insets is frozen. - mDisplayContent.mOpeningApps.clear(); - app.mActivityRecord.commitVisibility(false, false); - app.mActivityRecord.onWindowsGone(); - - assertTrue(app.mActivityRecord.mLastImeShown); - assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); - - // Expect IME insets frozen state will reset when the activity has no IME focusable window. - app.mActivityRecord.forAllWindows(w -> { - w.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM; - return true; - }, true); - - app.mActivityRecord.commitVisibility(true, false); - app.mActivityRecord.onWindowsVisible(); - - assertFalse(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); - } - - @SetupWindows(addWindows = W_INPUT_METHOD) - @Test - public void testImeInsetsFrozenFlag_resetWhenReportedToBeImeInputTarget() { - final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); - - mDisplayContent.getInsetsStateController().getImeSourceProvider().setWindowContainer( - mImeWindow, null, null); - mImeWindow.getControllableInsetProvider().setServerVisible(true); - - InsetsSource imeSource = new InsetsSource(ID_IME, ime()); - app.mAboveInsetsState.addSource(imeSource); - mDisplayContent.setImeLayeringTarget(app); - mDisplayContent.updateImeInputAndControlTarget(app); - - InsetsState state = app.getInsetsState(); - assertFalse(state.getOrCreateSource(imeSource.getId(), ime()).isVisible()); - assertTrue(state.getOrCreateSource(imeSource.getId(), ime()).getFrame().isEmpty()); - - // Simulate app is closing and expect IME insets is frozen. - mDisplayContent.mOpeningApps.clear(); - app.mActivityRecord.commitVisibility(false, false); - app.mActivityRecord.onWindowsGone(); - assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); - - // Simulate app re-start input or turning screen off/on then unlocked by un-secure - // keyguard to back to the app, expect IME insets is not frozen - app.mActivityRecord.commitVisibility(true, false); - mDisplayContent.updateImeInputAndControlTarget(app); - performSurfacePlacementAndWaitForWindowAnimator(); - - assertFalse(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); - - imeSource.setVisible(true); - imeSource.setFrame(new Rect(100, 400, 500, 500)); - app.mAboveInsetsState.addSource(imeSource); - - // Verify when IME is visible and the app can receive the right IME insets from policy. - makeWindowVisibleAndDrawn(app, mImeWindow); - state = app.getInsetsState(); - assertTrue(state.peekSource(ID_IME).isVisible()); - assertEquals(state.peekSource(ID_IME).getFrame(), imeSource.getFrame()); - } - - @SetupWindows(addWindows = { W_ACTIVITY, W_INPUT_METHOD }) - @Test - public void testImeInsetsFrozenFlag_noDispatchVisibleInsetsWhenAppNotRequest() - throws RemoteException { - final WindowState app1 = newWindowBuilder("app1", TYPE_APPLICATION).build(); - final WindowState app2 = newWindowBuilder("app2", TYPE_APPLICATION).build(); - - mDisplayContent.getInsetsStateController().getImeSourceProvider().setWindowContainer( - mImeWindow, null, null); - mImeWindow.getControllableInsetProvider().setServerVisible(true); - - // Simulate app2 is closing and let app1 is visible to be IME targets. - makeWindowVisibleAndDrawn(app1, mImeWindow); - mDisplayContent.setImeLayeringTarget(app1); - mDisplayContent.updateImeInputAndControlTarget(app1); - app2.mActivityRecord.commitVisibility(false, false); - - // app1 requests IME visible. - app1.setRequestedVisibleTypes(ime(), ime()); - mDisplayContent.getInsetsStateController().onRequestedVisibleTypesChanged(app1, - null /* statsToken */); - - // Verify app1's IME insets is visible and app2's IME insets frozen flag set. - assertTrue(app1.getInsetsState().peekSource(ID_IME).isVisible()); - assertTrue(app2.mActivityRecord.mImeInsetsFrozenUntilStartInput); - - // Simulate switching to app2 to make it visible to be IME targets. - spyOn(app2); - spyOn(app2.mClient); - spyOn(app2.getProcess()); - ArgumentCaptor<InsetsState> insetsStateCaptor = ArgumentCaptor.forClass(InsetsState.class); - doReturn(true).when(app2).isReadyToDispatchInsetsState(); - mDisplayContent.setImeLayeringTarget(app2); - app2.mActivityRecord.commitVisibility(true, false); - mDisplayContent.updateImeInputAndControlTarget(app2); - performSurfacePlacementAndWaitForWindowAnimator(); - - // Verify after unfreezing app2's IME insets state, we won't dispatch visible IME insets - // to client if the app didn't request IME visible. - assertFalse(app2.mActivityRecord.mImeInsetsFrozenUntilStartInput); - - verify(app2.getProcess(), atLeastOnce()).scheduleClientTransactionItem( - isA(WindowStateResizeItem.class)); - assertFalse(app2.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime())); - } - - @Test - public void testImeInsetsFrozenFlag_multiWindowActivities() { - final WindowToken imeToken = createTestWindowToken(TYPE_INPUT_METHOD, mDisplayContent); - final WindowState ime = newWindowBuilder("ime", TYPE_INPUT_METHOD).setWindowToken( - imeToken).build(); - makeWindowVisibleAndDrawn(ime); - - // Create a split-screen root task with activity1 and activity 2. - final Task task = new TaskBuilder(mSupervisor) - .setCreateParentTask(true).setCreateActivity(true).build(); - task.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); - final ActivityRecord activity1 = task.getTopNonFinishingActivity(); - activity1.getTask().setResumedActivity(activity1, "testApp1"); - - final ActivityRecord activity2 = new TaskBuilder(mSupervisor) - .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW) - .setCreateActivity(true).build().getTopMostActivity(); - activity2.getTask().setResumedActivity(activity2, "testApp2"); - activity2.getTask().setParent(task.getRootTask()); - - // Simulate activity1 and activity2 both have set mImeInsetsFrozenUntilStartInput when - // invisible to user. - activity1.mImeInsetsFrozenUntilStartInput = true; - activity2.mImeInsetsFrozenUntilStartInput = true; - - final WindowState app1 = newWindowBuilder("app1", TYPE_APPLICATION).setWindowToken( - activity1).build(); - final WindowState app2 = newWindowBuilder("app2", TYPE_APPLICATION).setWindowToken( - activity2).build(); - makeWindowVisibleAndDrawn(app1, app2); - - final InsetsStateController controller = mDisplayContent.getInsetsStateController(); - controller.getImeSourceProvider().setWindowContainer( - ime, null, null); - ime.getControllableInsetProvider().setServerVisible(true); - - // app1 starts input and expect IME insets for all activities in split-screen will be - // frozen until the input started. - mDisplayContent.setImeLayeringTarget(app1); - mDisplayContent.updateImeInputAndControlTarget(app1); - mDisplayContent.computeImeTarget(true /* updateImeTarget */); - performSurfacePlacementAndWaitForWindowAnimator(); - - assertEquals(app1, mDisplayContent.getImeInputTarget()); - assertFalse(activity1.mImeInsetsFrozenUntilStartInput); - assertFalse(activity2.mImeInsetsFrozenUntilStartInput); - - app1.setRequestedVisibleTypes(ime()); - controller.onRequestedVisibleTypesChanged(app1, null /* statsToken */); - - // Expect all activities in split-screen will get IME insets visible state - assertTrue(app1.getInsetsState().peekSource(ID_IME).isVisible()); - assertTrue(app2.getInsetsState().peekSource(ID_IME).isVisible()); - } - @Test public void testInClosingAnimation_visibilityNotCommitted_doNotHideSurface() { final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java index e0b700a4ffe3..eaffc481098e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java @@ -97,6 +97,7 @@ public class DesktopModeHelperTest { public void canEnterDesktopMode_DWFlagDisabled_configsOn_disableDeviceCheck_returnsFalse() throws Exception { doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)); + doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)); doReturn(true).when(mMockResources).getBoolean( eq(R.bool.config_isDesktopModeDevOptionSupported)); disableEnforceDeviceRestriction(); @@ -148,6 +149,7 @@ public class DesktopModeHelperTest { @Test public void canEnterDesktopMode_DWFlagEnabled_configDesktopModeOn_returnsTrue() { doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)); + doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)); assertThat(DesktopModeHelper.canEnterDesktopMode(mMockContext)).isTrue(); } @@ -176,21 +178,21 @@ public class DesktopModeHelperTest { @Test public void isDeviceEligibleForDesktopMode_configDEModeOn_returnsTrue() { - doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)); + doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)); - assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isTrue(); + assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isTrue(); } @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @Test public void isDeviceEligibleForDesktopMode_supportFlagOff_returnsFalse() { - assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isFalse(); + assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isFalse(); } @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @Test public void isDeviceEligibleForDesktopMode_supportFlagOn_returnsFalse() { - assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isFalse(); + assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isFalse(); } @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @@ -200,7 +202,7 @@ public class DesktopModeHelperTest { eq(R.bool.config_isDesktopModeDevOptionSupported) ); - assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isTrue(); + assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isTrue(); } private void resetEnforceDeviceRestriction() throws Exception { @@ -234,4 +236,4 @@ public class DesktopModeHelperTest { Settings.Global.putInt(mContext.getContentResolver(), DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, override.getSetting()); } -} +}
\ No newline at end of file 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 fdde3b38f19f..d305c2f54456 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.isDeviceEligibleForDesktopMode(any())); + .when(() -> DesktopModeHelper.canEnterDesktopMode(any())); doReturn(enforceDeviceRestrictions) .when(DesktopModeHelper::shouldEnforceDeviceRestrictions); } diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopWindowingRobot.java b/services/tests/wmtests/src/com/android/server/wm/DesktopWindowingRobot.java index 285a5e246e0c..ea21bb34597d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DesktopWindowingRobot.java +++ b/services/tests/wmtests/src/com/android/server/wm/DesktopWindowingRobot.java @@ -23,6 +23,7 @@ import static org.mockito.ArgumentMatchers.any; /** Robot for changing desktop windowing properties. */ class DesktopWindowingRobot { void allowEnterDesktopMode(boolean isAllowed) { - doReturn(isAllowed).when(() -> DesktopModeHelper.canEnterDesktopMode(any())); + doReturn(isAllowed).when(() -> + DesktopModeHelper.canEnterDesktopMode(any())); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java index 6c5fe1d8551e..71e34ef220d3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java @@ -53,6 +53,7 @@ import android.view.InsetsSource; import android.view.InsetsSourceControl; import android.view.InsetsState; import android.view.WindowInsets; +import android.view.WindowInsets.Type.InsetsType; import androidx.test.filters.SmallTest; @@ -400,9 +401,9 @@ public class InsetsPolicyTest extends WindowTestsBase { assertTrue(state.isSourceOrDefaultVisible(statusBarSource.getId(), statusBars())); assertTrue(state.isSourceOrDefaultVisible(navBarSource.getId(), navigationBars())); - mAppWindow.setRequestedVisibleTypes( + final @InsetsType int changedTypes = mAppWindow.setRequestedVisibleTypes( navigationBars() | statusBars(), navigationBars() | statusBars()); - policy.onRequestedVisibleTypesChanged(mAppWindow, null /* statsToken */); + policy.onRequestedVisibleTypesChanged(mAppWindow, changedTypes, null /* statsToken */); waitUntilWindowAnimatorIdle(); controls = mDisplayContent.getInsetsStateController().getControlsForDispatch(mAppWindow); diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java index 973c8d0a8464..5525bae89138 100644 --- a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java @@ -52,6 +52,7 @@ import android.util.SparseArray; import android.view.InsetsSource; import android.view.InsetsSourceControl; import android.view.InsetsState; +import android.view.WindowInsets.Type.InsetsType; import androidx.test.filters.SmallTest; @@ -201,8 +202,8 @@ public class InsetsStateControllerTest extends WindowTestsBase { getController().getOrCreateSourceProvider(ID_IME, ime()) .setWindowContainer(mImeWindow, null, null); getController().onImeControlTargetChanged(base); - base.setRequestedVisibleTypes(ime(), ime()); - getController().onRequestedVisibleTypesChanged(base, null /* statsToken */); + final @InsetsType int changedTypes = base.setRequestedVisibleTypes(ime(), ime()); + getController().onRequestedVisibleTypesChanged(base, changedTypes, null /* statsToken */); if (android.view.inputmethod.Flags.refactorInsetsController()) { // to set the serverVisibility, the IME needs to be drawn and onPostLayout be called. mImeWindow.mWinAnimator.mDrawState = HAS_DRAWN; @@ -509,8 +510,8 @@ public class InsetsStateControllerTest extends WindowTestsBase { mDisplayContent.setImeLayeringTarget(app); mDisplayContent.updateImeInputAndControlTarget(app); - app.setRequestedVisibleTypes(ime(), ime()); - getController().onRequestedVisibleTypesChanged(app, null /* statsToken */); + final @InsetsType int changedTypes = app.setRequestedVisibleTypes(ime(), ime()); + getController().onRequestedVisibleTypesChanged(app, changedTypes, null /* statsToken */); assertTrue(ime.getControllableInsetProvider().getSource().isVisible()); if (android.view.inputmethod.Flags.refactorInsetsController()) { diff --git a/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java new file mode 100644 index 000000000000..db90c28ec7df --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java @@ -0,0 +1,87 @@ +/* + * 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.server.wm; + +import static android.view.Display.FLAG_PRESENTATION; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.window.flags.Flags.FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; + +import android.graphics.Rect; +import android.os.UserHandle; +import android.os.UserManager; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.annotations.Presubmit; +import android.view.DisplayInfo; +import android.view.IWindow; +import android.view.InsetsSourceControl; +import android.view.InsetsState; +import android.view.View; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowManagerGlobal; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Build/Install/Run: + * atest WmTests:PresentationControllerTests + */ +@SmallTest +@Presubmit +@RunWith(WindowTestRunner.class) +public class PresentationControllerTests extends WindowTestsBase { + + @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS) + @Test + public void testPresentationHidesActivitiesBehind() { + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.copyFrom(mDisplayInfo); + displayInfo.flags = FLAG_PRESENTATION; + final DisplayContent dc = createNewDisplay(displayInfo); + final int displayId = dc.getDisplayId(); + doReturn(dc).when(mWm.mRoot).getDisplayContentOrCreate(displayId); + final ActivityRecord activity = createActivityRecord(createTask(dc)); + assertTrue(activity.isVisible()); + + doReturn(true).when(() -> UserManager.isVisibleBackgroundUsersEnabled()); + final int uid = 100000; // uid for non-system user + final Session session = createTestSession(mAtm, 1234 /* pid */, uid); + final int userId = UserHandle.getUserId(uid); + doReturn(false).when(mWm.mUmInternal).isUserVisible(eq(userId), eq(displayId)); + final WindowManager.LayoutParams params = new WindowManager.LayoutParams( + WindowManager.LayoutParams.TYPE_PRESENTATION); + + final IWindow clientWindow = new TestIWindow(); + final int result = mWm.addWindow(session, clientWindow, params, View.VISIBLE, displayId, + userId, WindowInsets.Type.defaultVisible(), null, new InsetsState(), + new InsetsSourceControl.Array(), new Rect(), new float[1]); + assertTrue(result >= WindowManagerGlobal.ADD_OKAY); + assertFalse(activity.isVisible()); + + final WindowState window = mWm.windowForClientLocked(session, clientWindow, false); + window.removeImmediately(); + assertTrue(activity.isVisible()); + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index 1323d8a59cef..71e84c0f1821 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -26,7 +26,6 @@ import static android.permission.flags.Flags.FLAG_SENSITIVE_CONTENT_RECENTS_SCRE import static android.permission.flags.Flags.FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.FLAG_OWN_FOCUS; -import static android.view.Display.FLAG_PRESENTATION; import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.FLAG_SECURE; @@ -55,7 +54,6 @@ import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_ import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING; import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR; import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER; -import static com.android.window.flags.Flags.FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS; import static com.google.common.truth.Truth.assertThat; @@ -102,7 +100,6 @@ import android.provider.Settings; import android.util.ArraySet; import android.util.MergedConfiguration; import android.view.ContentRecordingSession; -import android.view.DisplayInfo; import android.view.IWindow; import android.view.InputChannel; import android.view.InputDevice; @@ -1409,38 +1406,6 @@ public class WindowManagerServiceTests extends WindowTestsBase { assertEquals(activityWindowInfo2, activityWindowInfo3); } - @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS) - @Test - public void testPresentationHidesActivitiesBehind() { - DisplayInfo displayInfo = new DisplayInfo(); - displayInfo.copyFrom(mDisplayInfo); - displayInfo.flags = FLAG_PRESENTATION; - DisplayContent dc = createNewDisplay(displayInfo); - int displayId = dc.getDisplayId(); - doReturn(dc).when(mWm.mRoot).getDisplayContentOrCreate(displayId); - ActivityRecord activity = createActivityRecord(createTask(dc)); - assertTrue(activity.isVisible()); - - doReturn(true).when(() -> UserManager.isVisibleBackgroundUsersEnabled()); - int uid = 100000; // uid for non-system user - Session session = createTestSession(mAtm, 1234 /* pid */, uid); - int userId = UserHandle.getUserId(uid); - doReturn(false).when(mWm.mUmInternal).isUserVisible(eq(userId), eq(displayId)); - WindowManager.LayoutParams params = new WindowManager.LayoutParams( - LayoutParams.TYPE_PRESENTATION); - - final IWindow clientWindow = new TestIWindow(); - int result = mWm.addWindow(session, clientWindow, params, View.VISIBLE, displayId, - userId, WindowInsets.Type.defaultVisible(), null, new InsetsState(), - new InsetsSourceControl.Array(), new Rect(), new float[1]); - assertTrue(result >= WindowManagerGlobal.ADD_OKAY); - assertFalse(activity.isVisible()); - - final WindowState window = mWm.windowForClientLocked(session, clientWindow, false); - window.removeImmediately(); - assertTrue(activity.isVisible()); - } - @Test public void testAddOverlayWindowToUnassignedDisplay_notAllowed_ForVisibleBackgroundUsers() { doReturn(true).when(() -> UserManager.isVisibleBackgroundUsersEnabled()); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java index cff172f55601..a718c06cc2fa 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java @@ -1282,7 +1282,6 @@ public class WindowStateTests extends WindowTestsBase { // Simulate app plays closing transition to app2. app.mActivityRecord.commitVisibility(false, false); assertTrue(app.mActivityRecord.mLastImeShown); - assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); // Verify the IME insets is visible on app, but not for app2 during app task switching. assertTrue(app.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime())); @@ -1305,7 +1304,7 @@ public class WindowStateTests extends WindowTestsBase { // Simulate app2 in multi-window mode is going to background to switch to the fullscreen // app which requests IME with updating all windows Insets State when IME is above app. - app2.mActivityRecord.mImeInsetsFrozenUntilStartInput = true; + app2.mActivityRecord.setVisibleRequested(false); mDisplayContent.setImeLayeringTarget(app); mDisplayContent.setImeInputTarget(app); app.setRequestedVisibleTypes(ime(), ime()); @@ -1324,7 +1323,6 @@ public class WindowStateTests extends WindowTestsBase { mDisplayContent.setImeLayeringTarget(app2); app.mActivityRecord.commitVisibility(false, false); assertTrue(app.mActivityRecord.mLastImeShown); - assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); // Verify the IME insets is still visible on app, but not for app2 during task switching. assertTrue(app.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime())); diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index e0af22369182..d2741ac7ee9f 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -4821,10 +4821,14 @@ public class SubscriptionManager { + "Invalid subscriptionId: " + subscriptionId); } + String contextPkg = mContext != null ? mContext.getOpPackageName() : "<unknown>"; + String contextAttributionTag = mContext != null ? mContext.getAttributionTag() : null; + try { ISub iSub = TelephonyManager.getSubscriptionService(); if (iSub != null) { - return iSub.isSubscriptionAssociatedWithCallingUser(subscriptionId); + return iSub.isSubscriptionAssociatedWithCallingUser(subscriptionId, contextPkg, + contextAttributionTag); } else { throw new IllegalStateException("subscription service unavailable."); } diff --git a/telephony/java/android/telephony/data/ApnSetting.java b/telephony/java/android/telephony/data/ApnSetting.java index 5daa29b940bf..22624e22d534 100644 --- a/telephony/java/android/telephony/data/ApnSetting.java +++ b/telephony/java/android/telephony/data/ApnSetting.java @@ -994,7 +994,6 @@ public class ApnSetting implements Parcelable { * * @return True if the PDU session for this APN should always be on and false otherwise */ - @FlaggedApi(Flags.FLAG_APN_SETTING_FIELD_SUPPORT_FLAG) public boolean isAlwaysOn() { return mAlwaysOn; } @@ -2349,7 +2348,6 @@ public class ApnSetting implements Parcelable { * * @param alwaysOn the always on status to set for this APN */ - @FlaggedApi(Flags.FLAG_APN_SETTING_FIELD_SUPPORT_FLAG) public @NonNull Builder setAlwaysOn(boolean alwaysOn) { this.mAlwaysOn = alwaysOn; return this; diff --git a/telephony/java/com/android/internal/telephony/ISub.aidl b/telephony/java/com/android/internal/telephony/ISub.aidl index 1bfec29a3cf4..a974c615a4ae 100644 --- a/telephony/java/com/android/internal/telephony/ISub.aidl +++ b/telephony/java/com/android/internal/telephony/ISub.aidl @@ -347,13 +347,17 @@ interface ISub { * Returns whether the given subscription is associated with the calling user. * * @param subscriptionId the subscription ID of the subscription + * @param callingPackage The package maing the call + * @param callingFeatureId The feature in the package + * @return {@code true} if the subscription is associated with the user that the current process * is running in; {@code false} otherwise. * * @throws IllegalArgumentException if subscription doesn't exist. * @throws SecurityException if the caller doesn't have permissions required. */ - boolean isSubscriptionAssociatedWithCallingUser(int subscriptionId); + boolean isSubscriptionAssociatedWithCallingUser(int subscriptionId, String callingPackage, + String callingFeatureId); /** * Check if subscription and user are associated with each other. diff --git a/tests/AttestationVerificationTest/AndroidManifest.xml b/tests/AttestationVerificationTest/AndroidManifest.xml index 37321ad80b0f..758852bb1074 100644 --- a/tests/AttestationVerificationTest/AndroidManifest.xml +++ b/tests/AttestationVerificationTest/AndroidManifest.xml @@ -18,7 +18,7 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android.security.attestationverification"> - <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="30" /> + <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="34" /> <uses-permission android:name="android.permission.USE_ATTESTATION_VERIFICATION_SERVICE" /> <application> diff --git a/tests/AttestationVerificationTest/assets/test_revocation_list_no_test_certs.json b/tests/AttestationVerificationTest/assets/test_revocation_list_no_test_certs.json new file mode 100644 index 000000000000..2a3ba5ebde7d --- /dev/null +++ b/tests/AttestationVerificationTest/assets/test_revocation_list_no_test_certs.json @@ -0,0 +1,12 @@ +{ + "entries": { + "6681152659205225093" : { + "status": "REVOKED", + "reason": "KEY_COMPROMISE" + }, + "8350192447815228107" : { + "status": "REVOKED", + "reason": "KEY_COMPROMISE" + } + } +}
\ No newline at end of file diff --git a/tests/AttestationVerificationTest/assets/test_revocation_list_with_test_certs.json b/tests/AttestationVerificationTest/assets/test_revocation_list_with_test_certs.json new file mode 100644 index 000000000000..e22a834a92bf --- /dev/null +++ b/tests/AttestationVerificationTest/assets/test_revocation_list_with_test_certs.json @@ -0,0 +1,16 @@ +{ + "entries": { + "6681152659205225093" : { + "status": "REVOKED", + "reason": "KEY_COMPROMISE" + }, + "353017e73dc205a73a9c3de142230370" : { + "status": "REVOKED", + "reason": "KEY_COMPROMISE" + }, + "8350192447815228107" : { + "status": "REVOKED", + "reason": "KEY_COMPROMISE" + } + } +}
\ No newline at end of file diff --git a/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java b/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java new file mode 100644 index 000000000000..c38517ace5e6 --- /dev/null +++ b/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java @@ -0,0 +1,303 @@ +/* + * 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.server.security; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; + +import android.content.Context; +import android.os.SystemClock; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.security.cert.CertPathValidatorException; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RunWith(AndroidJUnit4.class) +public class CertificateRevocationStatusManagerTest { + + private static final String TEST_CERTIFICATE_FILE_1 = "test_attestation_with_root_certs.pem"; + private static final String TEST_CERTIFICATE_FILE_2 = "test_attestation_wrong_root_certs.pem"; + private static final String TEST_REVOCATION_LIST_FILE_NAME = "test_revocation_list.json"; + private static final String REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST = + "test_revocation_list_no_test_certs.json"; + private static final String REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST = + "test_revocation_list_with_test_certs.json"; + private static final String TEST_REVOCATION_STATUS_FILE_NAME = "test_revocation_status.txt"; + private static final String FILE_URL_PREFIX = "file://"; + private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); + + private CertificateFactory mFactory; + private List<X509Certificate> mCertificates1; + private List<X509Certificate> mCertificates2; + private File mRevocationListFile; + private String mRevocationListUrl; + private String mNonExistentRevocationListUrl; + private File mRevocationStatusFile; + private CertificateRevocationStatusManager mCertificateRevocationStatusManager; + + @Before + public void setUp() throws Exception { + mFactory = CertificateFactory.getInstance("X.509"); + mCertificates1 = getCertificateChain(TEST_CERTIFICATE_FILE_1); + mCertificates2 = getCertificateChain(TEST_CERTIFICATE_FILE_2); + mRevocationListFile = new File(mContext.getFilesDir(), TEST_REVOCATION_LIST_FILE_NAME); + mRevocationListUrl = FILE_URL_PREFIX + mRevocationListFile.getAbsolutePath(); + File noSuchFile = new File(mContext.getFilesDir(), "file_does_not_exist"); + mNonExistentRevocationListUrl = FILE_URL_PREFIX + noSuchFile.getAbsolutePath(); + mRevocationStatusFile = new File(mContext.getFilesDir(), TEST_REVOCATION_STATUS_FILE_NAME); + } + + @After + public void tearDown() throws Exception { + mRevocationListFile.delete(); + mRevocationStatusFile.delete(); + } + + @Test + public void checkRevocationStatus_doesNotExistOnRemoteRevocationList_noException() + throws Exception { + copyFromAssetToFile( + REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mRevocationStatusFile, false); + + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + } + + @Test + public void checkRevocationStatus_existsOnRemoteRevocationList_throwsException() + throws Exception { + copyFromAssetToFile( + REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mRevocationStatusFile, false); + + assertThrows( + CertPathValidatorException.class, + () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1)); + } + + @Test + public void + checkRevocationStatus_cannotReachRemoteRevocationList_noStoredStatus_throwsException() + throws Exception { + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false); + + assertThrows( + CertPathValidatorException.class, + () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1)); + } + + @Test + public void checkRevocationStatus_savesRevocationStatus() throws Exception { + copyFromAssetToFile( + REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mRevocationStatusFile, false); + + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + + assertThat(mRevocationStatusFile.length()).isGreaterThan(0); + } + + @Test + public void checkRevocationStatus_cannotReachRemoteList_certsSaved_noException() + throws Exception { + // call checkRevocationStatus once to save the revocation status + copyFromAssetToFile( + REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mRevocationStatusFile, false); + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + // call checkRevocationStatus again with mNonExistentRevocationListUrl + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false); + + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + } + + @Test + public void checkRevocationStatus_cannotReachRemoteList_someCertsNotSaved_exception() + throws Exception { + // call checkRevocationStatus once to save the revocation status for mCertificates2 + copyFromAssetToFile( + REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mRevocationStatusFile, false); + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates2); + // call checkRevocationStatus again with mNonExistentRevocationListUrl, this time for + // mCertificates1 + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false); + + assertThrows( + CertPathValidatorException.class, + () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1)); + } + + @Test + public void checkRevocationStatus_cannotReachRemoteList_someCertsStatusTooOld_exception() + throws Exception { + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false); + LocalDateTime now = LocalDateTime.now(); + LocalDateTime expiredStatusDate = + now.minusDays(CertificateRevocationStatusManager.MAX_DAYS_SINCE_LAST_CHECK + 1); + Map<String, LocalDateTime> lastRevocationCheckData = new HashMap<>(); + lastRevocationCheckData.put(getSerialNumber(mCertificates1.get(0)), expiredStatusDate); + for (int i = 1; i < mCertificates1.size(); i++) { + lastRevocationCheckData.put(getSerialNumber(mCertificates1.get(i)), now); + } + mCertificateRevocationStatusManager.storeLastRevocationCheckData(lastRevocationCheckData); + + assertThrows( + CertPathValidatorException.class, + () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1)); + } + + @Test + public void checkRevocationStatus_cannotReachRemoteList_allCertResultsFresh_noException() + throws Exception { + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false); + LocalDateTime bearlyNotExpiredStatusDate = + LocalDateTime.now() + .minusDays( + CertificateRevocationStatusManager.MAX_DAYS_SINCE_LAST_CHECK - 1); + Map<String, LocalDateTime> lastRevocationCheckData = new HashMap<>(); + for (X509Certificate certificate : mCertificates1) { + lastRevocationCheckData.put(getSerialNumber(certificate), bearlyNotExpiredStatusDate); + } + mCertificateRevocationStatusManager.storeLastRevocationCheckData(lastRevocationCheckData); + + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + } + + @Test + public void updateLastRevocationCheckData_correctlySavesStatus() throws Exception { + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false); + Map<String, Boolean> areCertificatesRevoked = new HashMap<>(); + for (X509Certificate certificate : mCertificates1) { + areCertificatesRevoked.put(getSerialNumber(certificate), false); + } + + mCertificateRevocationStatusManager.updateLastRevocationCheckData(areCertificatesRevoked); + + // no exception + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + // revoke one certificate and try again + areCertificatesRevoked.put(getSerialNumber(mCertificates1.getLast()), true); + mCertificateRevocationStatusManager.updateLastRevocationCheckData(areCertificatesRevoked); + assertThrows( + CertPathValidatorException.class, + () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1)); + } + + @Test + public void updateLastRevocationCheckDataForAllPreviouslySeenCertificates_updatesCorrectly() + throws Exception { + copyFromAssetToFile( + REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mRevocationStatusFile, false); + // populate the revocation status file + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + // Sleep for 2 second so that the current time changes + SystemClock.sleep(2000); + LocalDateTime timestampBeforeUpdate = LocalDateTime.now(); + JSONObject revocationList = mCertificateRevocationStatusManager.fetchRemoteRevocationList(); + List<String> otherCertificatesToCheck = new ArrayList<>(); + String serialNumber1 = "1234567"; // not revoked + String serialNumber2 = "8350192447815228107"; // revoked + String serialNumber3 = "987654"; // not revoked + otherCertificatesToCheck.add(serialNumber1); + otherCertificatesToCheck.add(serialNumber2); + otherCertificatesToCheck.add(serialNumber3); + + mCertificateRevocationStatusManager + .updateLastRevocationCheckDataForAllPreviouslySeenCertificates( + revocationList, otherCertificatesToCheck); + + Map<String, LocalDateTime> lastRevocationCheckData = + mCertificateRevocationStatusManager.getLastRevocationCheckData(); + assertThat(lastRevocationCheckData.get(serialNumber1)).isAtLeast(timestampBeforeUpdate); + assertThat(lastRevocationCheckData).doesNotContainKey(serialNumber2); // revoked + assertThat(lastRevocationCheckData.get(serialNumber3)).isAtLeast(timestampBeforeUpdate); + // validate that the existing certificates on the file got updated too + for (X509Certificate certificate : mCertificates1) { + assertThat(lastRevocationCheckData.get(getSerialNumber(certificate))) + .isAtLeast(timestampBeforeUpdate); + } + } + + private List<X509Certificate> getCertificateChain(String fileName) throws Exception { + Collection<? extends Certificate> certificates = + mFactory.generateCertificates(mContext.getResources().getAssets().open(fileName)); + ArrayList<X509Certificate> x509Certs = new ArrayList<>(); + for (Certificate cert : certificates) { + x509Certs.add((X509Certificate) cert); + } + return x509Certs; + } + + private void copyFromAssetToFile(String assetFileName, File targetFile) throws Exception { + byte[] data; + try (InputStream in = mContext.getResources().getAssets().open(assetFileName)) { + data = in.readAllBytes(); + } + try (FileOutputStream fileOutputStream = new FileOutputStream(targetFile)) { + fileOutputStream.write(data); + } + } + + private String getSerialNumber(X509Certificate certificate) { + return certificate.getSerialNumber().toString(16); + } +} diff --git a/tests/FlickerTests/IME/AndroidTestTemplate.xml b/tests/FlickerTests/IME/AndroidTestTemplate.xml index 12670cda74b2..ac704e5e7c39 100644 --- a/tests/FlickerTests/IME/AndroidTestTemplate.xml +++ b/tests/FlickerTests/IME/AndroidTestTemplate.xml @@ -52,10 +52,12 @@ <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="teardown-command" value="settings delete secure show_ime_with_hard_keyboard"/> <option name="teardown-command" value="settings delete system show_touches"/> <option name="teardown-command" value="settings delete system pointer_location"/> + <option name="teardown-command" value="settings delete secure glanceable_hub_enabled"/> <option name="teardown-command" value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/> </target_preparer> diff --git a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml index 481a8bb66fee..1b2007deae27 100644 --- a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml +++ b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml @@ -50,10 +50,12 @@ <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="teardown-command" value="settings delete secure show_ime_with_hard_keyboard"/> <option name="teardown-command" value="settings delete system show_touches"/> <option name="teardown-command" value="settings delete system pointer_location"/> + <option name="teardown-command" value="settings delete secure glanceable_hub_enabled"/> <option name="teardown-command" value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/> </target_preparer> diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt index 37bdf6b8614d..de47f013271a 100644 --- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt @@ -1438,6 +1438,58 @@ class KeyGestureControllerTests { ) } + @Test + @Parameters(method = "customInputGesturesTestArguments") + fun testCustomKeyGestureRestoredFromBackup(test: TestData) { + val userId = 10 + setupKeyGestureController() + val builder = InputGestureData.Builder() + .setKeyGestureType(test.expectedKeyGestureType) + .setTrigger( + InputGestureData.createKeyTrigger( + test.expectedKeys[0], + test.expectedModifierState + ) + ) + if (test.expectedAppLaunchData != null) { + builder.setAppLaunchData(test.expectedAppLaunchData) + } + val inputGestureData = builder.build() + + keyGestureController.setCurrentUserId(userId) + testLooper.dispatchAll() + keyGestureController.addCustomInputGesture(userId, inputGestureData.aidlData) + testLooper.dispatchAll() + val backupData = keyGestureController.getInputGestureBackupPayload(userId) + + // Delete the old data and reinitialize the controller simulating a "fresh" install. + tempFile.delete() + setupKeyGestureController() + keyGestureController.setCurrentUserId(userId) + testLooper.dispatchAll() + + // Initially there should be no gestures registered. + var savedInputGestures = keyGestureController.getCustomInputGestures(userId, null) + assertEquals( + "Test: $test doesn't produce correct number of saved input gestures", + 0, + savedInputGestures.size + ) + + // After the restore, there should be the original gesture re-registered. + keyGestureController.applyInputGesturesBackupPayload(backupData, userId) + savedInputGestures = keyGestureController.getCustomInputGestures(userId, null) + assertEquals( + "Test: $test doesn't produce correct number of saved input gestures", + 1, + savedInputGestures.size + ) + assertEquals( + "Test: $test doesn't produce correct input gesture data", inputGestureData, + InputGestureData(savedInputGestures[0]) + ) + } + class TouchpadTestData( val name: String, val touchpadGestureType: Int, @@ -1549,6 +1601,53 @@ class KeyGestureControllerTests { ) } + + @Test + @Parameters(method = "customTouchpadGesturesTestArguments") + fun testCustomTouchpadGesturesRestoredFromBackup(test: TouchpadTestData) { + val userId = 10 + setupKeyGestureController() + val builder = InputGestureData.Builder() + .setKeyGestureType(test.expectedKeyGestureType) + .setTrigger(InputGestureData.createTouchpadTrigger(test.touchpadGestureType)) + if (test.expectedAppLaunchData != null) { + builder.setAppLaunchData(test.expectedAppLaunchData) + } + val inputGestureData = builder.build() + keyGestureController.setCurrentUserId(userId) + testLooper.dispatchAll() + keyGestureController.addCustomInputGesture(userId, inputGestureData.aidlData) + testLooper.dispatchAll() + val backupData = keyGestureController.getInputGestureBackupPayload(userId) + + // Delete the old data and reinitialize the controller simulating a "fresh" install. + tempFile.delete() + setupKeyGestureController() + keyGestureController.setCurrentUserId(userId) + testLooper.dispatchAll() + + // Initially there should be no gestures registered. + var savedInputGestures = keyGestureController.getCustomInputGestures(userId, null) + assertEquals( + "Test: $test doesn't produce correct number of saved input gestures", + 0, + savedInputGestures.size + ) + + // After the restore, there should be the original gesture re-registered. + keyGestureController.applyInputGesturesBackupPayload(backupData, userId) + savedInputGestures = keyGestureController.getCustomInputGestures(userId, null) + assertEquals( + "Test: $test doesn't produce correct number of saved input gestures", + 1, + savedInputGestures.size + ) + assertEquals( + "Test: $test doesn't produce correct input gesture data", inputGestureData, + InputGestureData(savedInputGestures[0]) + ) + } + private fun testKeyGestureInternal(test: TestData) { val handledEvents = mutableListOf<KeyGestureEvent>() val handler = KeyGestureHandler { event, _ -> diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java index 3ee6dc48bfa3..e20cd079bfbd 100644 --- a/tests/testables/src/android/testing/TestableLooper.java +++ b/tests/testables/src/android/testing/TestableLooper.java @@ -16,6 +16,7 @@ package android.testing; import android.annotation.NonNull; import android.annotation.Nullable; +import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; @@ -24,7 +25,7 @@ import android.os.MessageQueue; import android.os.TestLooperManager; import android.util.ArrayMap; -import androidx.test.InstrumentationRegistry; +import androidx.test.platform.app.InstrumentationRegistry; import org.junit.runners.model.FrameworkMethod; @@ -33,8 +34,10 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Field; +import java.util.ArrayDeque; import java.util.Map; import java.util.Objects; +import java.util.Queue; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -67,16 +70,42 @@ public class TestableLooper { private Handler mHandler; private TestLooperManager mQueueWrapper; - static { + /** + * Baklava introduces new {@link TestLooperManager} APIs that we can use instead of reflection. + */ + private static boolean isAtLeastBaklava() { + TestLooperManager tlm = + InstrumentationRegistry.getInstrumentation() + .acquireLooperManager(Looper.getMainLooper()); try { - 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); - } catch (NoSuchFieldException e) { - throw new RuntimeException("Failed to initialize TestableLooper", e); + Long unused = tlm.peekWhen(); + return true; + } catch (NoSuchMethodError e) { + return false; + } finally { + tlm.release(); + } + // TODO(shayba): delete the above, uncomment the below. + // SDK_INT has not yet ramped to Baklava in all 25Q2 builds. + // return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA; + } + + static { + if (isAtLeastBaklava()) { + MESSAGE_QUEUE_MESSAGES_FIELD = null; + MESSAGE_NEXT_FIELD = null; + MESSAGE_WHEN_FIELD = null; + } else { + try { + 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); + } catch (NoSuchFieldException e) { + throw new RuntimeException("Failed to initialize TestableLooper", e); + } } } @@ -222,8 +251,61 @@ public class TestableLooper { } 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 = mQueueWrapper.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); + mQueueWrapper.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 = getMessageLinkedList(); + Message msg = (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(mLooper.getQueue()); while (msg != null) { long updatedWhen = msg.getWhen() - milliSeconds; if (updatedWhen < 0) { @@ -237,17 +319,6 @@ public class TestableLooper { } } - 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 TestableLooper: get - MessageQueue.mMessages", - e); - } - } - private int processQueuedMessages() { int count = 0; Runnable barrierRunnable = () -> { }; diff --git a/tools/aapt2/cmd/Command.cpp b/tools/aapt2/cmd/Command.cpp index f00a6cad6b46..20315561cceb 100644 --- a/tools/aapt2/cmd/Command.cpp +++ b/tools/aapt2/cmd/Command.cpp @@ -54,9 +54,7 @@ std::string GetSafePath(StringPiece arg) { void Command::AddRequiredFlag(StringPiece name, StringPiece description, std::string* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - if (value) { - *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); - } + *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); return true; }; @@ -67,9 +65,7 @@ void Command::AddRequiredFlag(StringPiece name, StringPiece description, std::st void Command::AddRequiredFlagList(StringPiece name, StringPiece description, std::vector<std::string>* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - if (value) { - value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); - } + value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); return true; }; @@ -80,9 +76,7 @@ void Command::AddRequiredFlagList(StringPiece name, StringPiece description, void Command::AddOptionalFlag(StringPiece name, StringPiece description, std::optional<std::string>* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - if (value) { - *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); - } + *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); return true; }; @@ -93,9 +87,7 @@ void Command::AddOptionalFlag(StringPiece name, StringPiece description, void Command::AddOptionalFlagList(StringPiece name, StringPiece description, std::vector<std::string>* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - if (value) { - value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); - } + value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); return true; }; @@ -106,9 +98,7 @@ void Command::AddOptionalFlagList(StringPiece name, StringPiece description, void Command::AddOptionalFlagList(StringPiece name, StringPiece description, std::unordered_set<std::string>* value) { auto func = [value](StringPiece arg, std::ostream* out_error) -> bool { - if (value) { - value->emplace(arg); - } + value->emplace(arg); return true; }; @@ -118,9 +108,7 @@ void Command::AddOptionalFlagList(StringPiece name, StringPiece description, void Command::AddOptionalSwitch(StringPiece name, StringPiece description, bool* value) { auto func = [value](StringPiece arg, std::ostream* out_error) -> bool { - if (value) { - *value = true; - } + *value = true; return true; }; diff --git a/tools/aapt2/cmd/Command_test.cpp b/tools/aapt2/cmd/Command_test.cpp index ad167c979662..2a3cb2a0c65d 100644 --- a/tools/aapt2/cmd/Command_test.cpp +++ b/tools/aapt2/cmd/Command_test.cpp @@ -159,22 +159,4 @@ TEST(CommandTest, ShortOptions) { ASSERT_NE(0, command.Execute({"-w"s, "2"s}, &std::cerr)); } -TEST(CommandTest, OptionsWithNullptrToAcceptValues) { - TestCommand command; - command.AddRequiredFlag("--rflag", "", nullptr); - command.AddRequiredFlagList("--rlflag", "", nullptr); - command.AddOptionalFlag("--oflag", "", nullptr); - command.AddOptionalFlagList("--olflag", "", (std::vector<std::string>*)nullptr); - command.AddOptionalFlagList("--olflag2", "", (std::unordered_set<std::string>*)nullptr); - command.AddOptionalSwitch("--switch", "", nullptr); - - ASSERT_EQ(0, command.Execute({ - "--rflag"s, "1"s, - "--rlflag"s, "1"s, - "--oflag"s, "1"s, - "--olflag"s, "1"s, - "--olflag2"s, "1"s, - "--switch"s}, &std::cerr)); -} - } // namespace aapt
\ No newline at end of file diff --git a/tools/aapt2/cmd/Convert.cpp b/tools/aapt2/cmd/Convert.cpp index 060bc5fa2242..6c3eae11eab9 100644 --- a/tools/aapt2/cmd/Convert.cpp +++ b/tools/aapt2/cmd/Convert.cpp @@ -425,6 +425,9 @@ int ConvertCommand::Action(const std::vector<std::string>& args) { << output_format_.value()); return 1; } + if (enable_sparse_encoding_) { + table_flattener_options_.sparse_entries = SparseEntriesMode::Enabled; + } if (force_sparse_encoding_) { table_flattener_options_.sparse_entries = SparseEntriesMode::Forced; } diff --git a/tools/aapt2/cmd/Convert.h b/tools/aapt2/cmd/Convert.h index 98c8f5ff89c0..9452e588953e 100644 --- a/tools/aapt2/cmd/Convert.h +++ b/tools/aapt2/cmd/Convert.h @@ -36,9 +36,11 @@ class ConvertCommand : public Command { kOutputFormatProto, kOutputFormatBinary, kOutputFormatBinary), &output_format_); AddOptionalSwitch( "--enable-sparse-encoding", - "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n" - "enabled if minSdk of the APK is >= 32.", - nullptr); + "Enables encoding sparse entries using a binary search tree.\n" + "This decreases APK size at the cost of resource retrieval performance.\n" + "Only applies sparse encoding to Android O+ resources or all resources if minSdk of " + "the APK is O+", + &enable_sparse_encoding_); AddOptionalSwitch("--force-sparse-encoding", "Enables encoding sparse entries using a binary search tree.\n" "This decreases APK size at the cost of resource retrieval performance.\n" @@ -85,6 +87,7 @@ class ConvertCommand : public Command { std::string output_path_; std::optional<std::string> output_format_; bool verbose_ = false; + bool enable_sparse_encoding_ = false; bool force_sparse_encoding_ = false; bool enable_compact_entries_ = false; std::optional<std::string> resources_config_path_; diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index eb71189ffc46..ff4d8ef2ec25 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -674,7 +674,8 @@ bool ResourceFileFlattener::Flatten(ResourceTable* table, IArchiveWriter* archiv } FeatureFlagsFilterOptions flags_filter_options; - flags_filter_options.flags_must_be_readonly = true; + flags_filter_options.fail_on_unrecognized_flags = false; + flags_filter_options.flags_must_have_value = false; FeatureFlagsFilter flags_filter(options_.feature_flag_values, flags_filter_options); if (!flags_filter.Consume(context_, doc.get())) { return 1; @@ -2504,6 +2505,9 @@ int LinkCommand::Action(const std::vector<std::string>& args) { << "the --merge-only flag can be only used when building a static library"); return 1; } + if (options_.use_sparse_encoding) { + options_.table_flattener_options.sparse_entries = SparseEntriesMode::Enabled; + } // The default build type. context.SetPackageType(PackageType::kApp); diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h index b5bd905c02be..2f17853718ec 100644 --- a/tools/aapt2/cmd/Link.h +++ b/tools/aapt2/cmd/Link.h @@ -75,6 +75,7 @@ struct LinkOptions { bool no_resource_removal = false; bool no_xml_namespaces = false; bool do_not_compress_anything = false; + bool use_sparse_encoding = false; std::unordered_set<std::string> extensions_to_not_compress; std::optional<std::regex> regex_to_not_compress; FeatureFlagValues feature_flag_values; @@ -162,11 +163,9 @@ class LinkCommand : public Command { AddOptionalSwitch("--no-resource-removal", "Disables automatic removal of resources without\n" "defaults. Use this only when building runtime resource overlay packages.", &options_.no_resource_removal); - AddOptionalSwitch( - "--enable-sparse-encoding", - "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n" - "enabled if minSdk of the APK is >= 32.", - nullptr); + AddOptionalSwitch("--enable-sparse-encoding", + "This decreases APK size at the cost of resource retrieval performance.", + &options_.use_sparse_encoding); AddOptionalSwitch("--enable-compact-entries", "This decreases APK size by using compact resource entries for simple data types.", &options_.table_flattener_options.use_compact_entries); diff --git a/tools/aapt2/cmd/Link_test.cpp b/tools/aapt2/cmd/Link_test.cpp index 6cc42f17c0a1..a2dc8f8ce0fd 100644 --- a/tools/aapt2/cmd/Link_test.cpp +++ b/tools/aapt2/cmd/Link_test.cpp @@ -1026,7 +1026,7 @@ TEST_F(LinkTest, FeatureFlagDisabled_SdkAtMostUDC) { .SetManifestFile(app_manifest) .AddParameter("-I", android_apk) .AddParameter("--java", app_java) - .AddParameter("--feature-flags", "flag=false"); + .AddParameter("--feature-flags", "flag:ro=false"); const std::string app_apk = GetTestPath("app.apk"); BuildApk({}, app_apk, std::move(app_link_args), this, &diag); diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp index f218307af578..762441ee1872 100644 --- a/tools/aapt2/cmd/Optimize.cpp +++ b/tools/aapt2/cmd/Optimize.cpp @@ -406,6 +406,9 @@ int OptimizeCommand::Action(const std::vector<std::string>& args) { return 1; } + if (options_.enable_sparse_encoding) { + options_.table_flattener_options.sparse_entries = SparseEntriesMode::Enabled; + } if (options_.force_sparse_encoding) { options_.table_flattener_options.sparse_entries = SparseEntriesMode::Forced; } diff --git a/tools/aapt2/cmd/Optimize.h b/tools/aapt2/cmd/Optimize.h index e3af584cbbd9..012b0f230ca2 100644 --- a/tools/aapt2/cmd/Optimize.h +++ b/tools/aapt2/cmd/Optimize.h @@ -61,6 +61,9 @@ struct OptimizeOptions { // TODO(b/246489170): keep the old option and format until transform to the new one std::optional<std::string> shortened_paths_map_path; + // Whether sparse encoding should be used for O+ resources. + bool enable_sparse_encoding = false; + // Whether sparse encoding should be used for all resources. bool force_sparse_encoding = false; @@ -103,9 +106,11 @@ class OptimizeCommand : public Command { &kept_artifacts_); AddOptionalSwitch( "--enable-sparse-encoding", - "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n" - "enabled if minSdk of the APK is >= 32.", - nullptr); + "Enables encoding sparse entries using a binary search tree.\n" + "This decreases APK size at the cost of resource retrieval performance.\n" + "Only applies sparse encoding to Android O+ resources or all resources if minSdk of " + "the APK is O+", + &options_.enable_sparse_encoding); AddOptionalSwitch("--force-sparse-encoding", "Enables encoding sparse entries using a binary search tree.\n" "This decreases APK size at the cost of resource retrieval performance.\n" diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp index b8ac7925d44e..1a82021bce71 100644 --- a/tools/aapt2/format/binary/TableFlattener.cpp +++ b/tools/aapt2/format/binary/TableFlattener.cpp @@ -201,7 +201,7 @@ class PackageFlattener { (context_->GetMinSdkVersion() == 0 && config.sdkVersion == 0)) { // Sparse encode if forced or sdk version is not set in context and config. } else { - // Otherwise, only sparse encode if the entries will be read on platforms S_V2+ (32). + // Otherwise, only sparse encode if the entries will be read on platforms S_V2+. sparse_encode = sparse_encode && (context_->GetMinSdkVersion() >= SDK_S_V2); } diff --git a/tools/aapt2/format/binary/TableFlattener.h b/tools/aapt2/format/binary/TableFlattener.h index f1c4c3512ed3..0633bc81cb25 100644 --- a/tools/aapt2/format/binary/TableFlattener.h +++ b/tools/aapt2/format/binary/TableFlattener.h @@ -37,7 +37,8 @@ constexpr const size_t kSparseEncodingThreshold = 60; enum class SparseEntriesMode { // Disables sparse encoding for entries. Disabled, - // Enables sparse encoding for all entries for APKs with minSdk >= 32 (S_V2). + // Enables sparse encoding for all entries for APKs with O+ minSdk. For APKs with minSdk less + // than O only applies sparse encoding for resource configuration available on O+. Enabled, // Enables sparse encoding for all entries regardless of minSdk. Forced, @@ -46,7 +47,7 @@ enum class SparseEntriesMode { struct TableFlattenerOptions { // When enabled, types for configurations with a sparse set of entries are encoded // as a sparse map of entry ID and offset to actual data. - SparseEntriesMode sparse_entries = SparseEntriesMode::Enabled; + SparseEntriesMode sparse_entries = SparseEntriesMode::Disabled; // When true, use compact entries for simple data bool use_compact_entries = false; diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp index e3d589eb078b..0f1168514c4a 100644 --- a/tools/aapt2/format/binary/TableFlattener_test.cpp +++ b/tools/aapt2/format/binary/TableFlattener_test.cpp @@ -337,13 +337,13 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkSV2) { auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f); TableFlattenerOptions options; - options.sparse_entries = SparseEntriesMode::Disabled; + options.sparse_entries = SparseEntriesMode::Enabled; std::string no_sparse_contents; - ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &no_sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents)); std::string sparse_contents; - ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents)); EXPECT_GT(no_sparse_contents.size(), sparse_contents.size()); @@ -421,13 +421,13 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryWithSdkVersionNotSet) { auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f); TableFlattenerOptions options; - options.sparse_entries = SparseEntriesMode::Disabled; + options.sparse_entries = SparseEntriesMode::Enabled; std::string no_sparse_contents; - ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &no_sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents)); std::string sparse_contents; - ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents)); EXPECT_GT(no_sparse_contents.size(), sparse_contents.size()); diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/layout/layout1.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/layout/layout1.xml index 8b9ce134a9de..c595cdcff482 100644 --- a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/layout/layout1.xml +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/layout/layout1.xml @@ -6,12 +6,14 @@ <TextView android:id="@+id/text1" android:layout_width="wrap_content" - android:layout_height="wrap_content"/> + android:layout_height="wrap_content" + android:featureFlag="test.package.readWriteFlag"/> <TextView android:id="@+id/disabled_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:featureFlag="test.package.falseFlag" /> <TextView android:id="@+id/text2" + android:text="FIND_ME" android:layout_width="wrap_content" android:layout_height="wrap_content" android:featureFlag="test.package.trueFlag" /> diff --git a/tools/aapt2/link/FeatureFlagsFilter.cpp b/tools/aapt2/link/FeatureFlagsFilter.cpp index 4e7c1b4d8e54..23f78388b930 100644 --- a/tools/aapt2/link/FeatureFlagsFilter.cpp +++ b/tools/aapt2/link/FeatureFlagsFilter.cpp @@ -50,7 +50,7 @@ class FlagsVisitor : public xml::Visitor { private: bool ShouldRemove(std::unique_ptr<xml::Node>& node) { - if (const auto* el = NodeCast<Element>(node.get())) { + if (auto* el = NodeCast<Element>(node.get())) { auto* attr = el->FindAttribute(xml::kSchemaAndroid, "featureFlag"); if (attr == nullptr) { return false; @@ -72,9 +72,13 @@ class FlagsVisitor : public xml::Visitor { has_error_ = true; return false; } - if (options_.remove_disabled_elements) { + if (options_.remove_disabled_elements && it->second.read_only) { // Remove if flag==true && attr=="!flag" (negated) OR flag==false && attr=="flag" - return *it->second.enabled == negated; + bool remove = *it->second.enabled == negated; + if (!remove) { + el->RemoveAttribute(xml::kSchemaAndroid, "featureFlag"); + } + return remove; } } else if (options_.flags_must_have_value) { diagnostics_->Error(android::DiagMessage(node->line_number) diff --git a/tools/aapt2/link/FeatureFlagsFilter_test.cpp b/tools/aapt2/link/FeatureFlagsFilter_test.cpp index 2db2899e716c..744045588506 100644 --- a/tools/aapt2/link/FeatureFlagsFilter_test.cpp +++ b/tools/aapt2/link/FeatureFlagsFilter_test.cpp @@ -48,7 +48,7 @@ TEST(FeatureFlagsFilterTest, NoFeatureFlagAttributes) { <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android"> <permission android:name="FOO" /> </manifest>)EOF", - {{"flag", FeatureFlagProperties{false, false}}}); + {{"flag", FeatureFlagProperties{true, false}}}); ASSERT_THAT(doc, NotNull()); auto root = doc->root.get(); ASSERT_THAT(root, NotNull()); @@ -60,7 +60,7 @@ TEST(FeatureFlagsFilterTest, RemoveElementWithDisabledFlag) { <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android"> <permission android:name="FOO" android:featureFlag="flag" /> </manifest>)EOF", - {{"flag", FeatureFlagProperties{false, false}}}); + {{"flag", FeatureFlagProperties{true, false}}}); ASSERT_THAT(doc, NotNull()); auto root = doc->root.get(); ASSERT_THAT(root, NotNull()); @@ -73,7 +73,7 @@ TEST(FeatureFlagsFilterTest, RemoveElementWithNegatedEnabledFlag) { <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android"> <permission android:name="FOO" android:featureFlag="!flag" /> </manifest>)EOF", - {{"flag", FeatureFlagProperties{false, true}}}); + {{"flag", FeatureFlagProperties{true, true}}}); ASSERT_THAT(doc, NotNull()); auto root = doc->root.get(); ASSERT_THAT(root, NotNull()); @@ -86,7 +86,7 @@ TEST(FeatureFlagsFilterTest, KeepElementWithEnabledFlag) { <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android"> <permission android:name="FOO" android:featureFlag="flag" /> </manifest>)EOF", - {{"flag", FeatureFlagProperties{false, true}}}); + {{"flag", FeatureFlagProperties{true, true}}}); ASSERT_THAT(doc, NotNull()); auto root = doc->root.get(); ASSERT_THAT(root, NotNull()); @@ -102,7 +102,7 @@ TEST(FeatureFlagsFilterTest, SideBySideEnabledAndDisabled) { <permission android:name="FOO" android:featureFlag="flag" android:protectionLevel="dangerous" /> </manifest>)EOF", - {{"flag", FeatureFlagProperties{false, true}}}); + {{"flag", FeatureFlagProperties{true, true}}}); ASSERT_THAT(doc, NotNull()); auto root = doc->root.get(); ASSERT_THAT(root, NotNull()); @@ -123,7 +123,7 @@ TEST(FeatureFlagsFilterTest, RemoveDeeplyNestedElement) { </activity> </application> </manifest>)EOF", - {{"flag", FeatureFlagProperties{false, true}}}); + {{"flag", FeatureFlagProperties{true, true}}}); ASSERT_THAT(doc, NotNull()); auto root = doc->root.get(); ASSERT_THAT(root, NotNull()); @@ -145,7 +145,7 @@ TEST(FeatureFlagsFilterTest, KeepDeeplyNestedElement) { </activity> </application> </manifest>)EOF", - {{"flag", FeatureFlagProperties{false, true}}}); + {{"flag", FeatureFlagProperties{true, true}}}); ASSERT_THAT(doc, NotNull()); auto root = doc->root.get(); ASSERT_THAT(root, NotNull()); @@ -162,7 +162,7 @@ TEST(FeatureFlagsFilterTest, FailOnEmptyFeatureFlagAttribute) { <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android"> <permission android:name="FOO" android:featureFlag=" " /> </manifest>)EOF", - {{"flag", FeatureFlagProperties{false, false}}}); + {{"flag", FeatureFlagProperties{true, false}}}); ASSERT_THAT(doc, IsNull()); } @@ -171,7 +171,7 @@ TEST(FeatureFlagsFilterTest, FailOnFlagWithNoGivenValue) { <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android"> <permission android:name="FOO" android:featureFlag="flag" /> </manifest>)EOF", - {{"flag", FeatureFlagProperties{false, std::nullopt}}}); + {{"flag", FeatureFlagProperties{true, std::nullopt}}}); ASSERT_THAT(doc, IsNull()); } @@ -180,7 +180,7 @@ TEST(FeatureFlagsFilterTest, FailOnUnrecognizedFlag) { <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android"> <permission android:name="FOO" android:featureFlag="unrecognized" /> </manifest>)EOF", - {{"flag", FeatureFlagProperties{false, true}}}); + {{"flag", FeatureFlagProperties{true, true}}}); ASSERT_THAT(doc, IsNull()); } @@ -190,7 +190,7 @@ TEST(FeatureFlagsFilterTest, FailOnMultipleValidationErrors) { <permission android:name="FOO" android:featureFlag="bar" /> <permission android:name="FOO" android:featureFlag="unrecognized" /> </manifest>)EOF", - {{"flag", FeatureFlagProperties{false, std::nullopt}}}); + {{"flag", FeatureFlagProperties{true, std::nullopt}}}); ASSERT_THAT(doc, IsNull()); } @@ -199,7 +199,7 @@ TEST(FeatureFlagsFilterTest, OptionRemoveDisabledElementsIsFalse) { <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android"> <permission android:name="FOO" android:featureFlag="flag" /> </manifest>)EOF", - {{"flag", FeatureFlagProperties{false, false}}}, + {{"flag", FeatureFlagProperties{true, false}}}, {.remove_disabled_elements = false}); ASSERT_THAT(doc, NotNull()); auto root = doc->root.get(); @@ -213,7 +213,7 @@ TEST(FeatureFlagsFilterTest, OptionFlagsMustHaveValueIsFalse) { <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android"> <permission android:name="FOO" android:featureFlag="flag" /> </manifest>)EOF", - {{"flag", FeatureFlagProperties{false, std::nullopt}}}, + {{"flag", FeatureFlagProperties{true, std::nullopt}}}, {.flags_must_have_value = false}); ASSERT_THAT(doc, NotNull()); auto root = doc->root.get(); @@ -227,7 +227,7 @@ TEST(FeatureFlagsFilterTest, OptionFailOnUnrecognizedFlagsIsFalse) { <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android"> <permission android:name="FOO" android:featureFlag="unrecognized" /> </manifest>)EOF", - {{"flag", FeatureFlagProperties{false, true}}}, + {{"flag", FeatureFlagProperties{true, true}}}, {.fail_on_unrecognized_flags = false}); ASSERT_THAT(doc, NotNull()); auto root = doc->root.get(); diff --git a/tools/aapt2/link/FlaggedResources_test.cpp b/tools/aapt2/link/FlaggedResources_test.cpp index 629300838bbe..adf711ecfcbb 100644 --- a/tools/aapt2/link/FlaggedResources_test.cpp +++ b/tools/aapt2/link/FlaggedResources_test.cpp @@ -59,6 +59,16 @@ void DumpChunksToString(LoadedApk* loaded_apk, std::string* output) { output_stream.Flush(); } +void DumpXmlTreeToString(LoadedApk* loaded_apk, std::string file, std::string* output) { + StringOutputStream output_stream(output); + Printer printer(&output_stream); + + auto xml = loaded_apk->LoadXml(file, &noop_diag); + ASSERT_NE(xml, nullptr); + Debug::DumpXml(*xml, &printer); + output_stream.Flush(); +} + TEST_F(FlaggedResourcesTest, DisabledStringRemovedFromPool) { auto apk_path = file::BuildPath({android::base::GetExecutableDirectory(), "resapp.apk"}); auto loaded_apk = LoadedApk::LoadApkFromPath(apk_path, &noop_diag); @@ -148,4 +158,15 @@ TEST_F(FlaggedResourcesTest, TwoValuesSameDisabledFlagDifferentFiles) { ASSERT_TRUE(diag.GetLog().contains("duplicate value for resource 'bool1'")); } +TEST_F(FlaggedResourcesTest, EnabledXmlELementAttributeRemoved) { + auto apk_path = file::BuildPath({android::base::GetExecutableDirectory(), "resapp.apk"}); + auto loaded_apk = LoadedApk::LoadApkFromPath(apk_path, &noop_diag); + + std::string output; + DumpXmlTreeToString(loaded_apk.get(), "res/layout-v22/layout1.xml", &output); + ASSERT_FALSE(output.contains("test.package.trueFlag")); + ASSERT_TRUE(output.contains("FIND_ME")); + ASSERT_TRUE(output.contains("test.package.readWriteFlag")); +} + } // namespace aapt diff --git a/tools/aapt2/readme.md b/tools/aapt2/readme.md index 664d8412a3be..5c3dfdcadfec 100644 --- a/tools/aapt2/readme.md +++ b/tools/aapt2/readme.md @@ -3,8 +3,6 @@ ## Version 2.20 - Too many features, bug fixes, and improvements to list since the last minor version update in 2017. This README will be updated more frequently in the future. -- Sparse encoding is now always enabled by default if the minSdkVersion is >= 32 (S_V2). The - `--enable-sparse-encoding` flag still exists, but is a no-op. ## Version 2.19 - Added navigation resource type. |