diff options
327 files changed, 7727 insertions, 4254 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/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/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/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/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/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/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/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/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/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/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/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index d413ba0b042c..09c6dc0e2b20 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." 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/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/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..d7ffcc54562c 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,8 @@ <!-- Array containing the notification assistant service adjustments that are not supported by default on this device--> <string-array translatable="false" name="config_notificationDefaultUnsupportedAdjustments" /> + + <!-- 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..6701e63c4f90 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,8 @@ <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" /> + + <!-- 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/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/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/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/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/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/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..35802c936361 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 @@ -58,11 +58,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 +80,7 @@ class DragZoneFactory( } else { dragZones.addAll(createSplitScreenDragZonesForExpandedViewOnTablet()) } - createBubbleDragZonesForExpandedView() + dragZones.addAll(createBubbleHalfScreenDragZones()) } } return dragZones @@ -98,7 +98,7 @@ class DragZoneFactory( ) } - private fun createBubbleDragZones(): List<DragZone> { + private fun createBubbleCornerDragZones(): List<DragZone> { val dragZoneSize = if (deviceConfig.isSmallTablet) { bubbleDragZoneFoldableSize @@ -124,7 +124,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/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..8cf2370df48d 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; @@ -872,11 +874,19 @@ 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(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,9 +1516,16 @@ 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; + 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(info); // Removes from overflow ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - shortcut=%s", info); if (b.isInflated()) { @@ -1524,7 +1541,25 @@ public class BubbleController implements ConfigurationChangeListener, * * @param intent the intent for the bubble. */ - public void expandStackAndSelectBubble(Intent intent, UserHandle user, + public void expandStackAndSelectBubble(Intent intent, UserHandle user) { + if (!BubbleAnythingFlagHelper.enableCreateAnyBubble()) return; + Bubble b = mBubbleData.getOrCreateBubble(intent, user); // Removes from overflow + ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - intent=%s", intent); + if (b.isInflated()) { + mBubbleData.setSelectedBubbleAndExpandStack(b); + } else { + b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); + inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); + } + } + + /** + * 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; if (bubbleBarLocation != null) { @@ -1532,8 +1567,9 @@ public class BubbleController implements ConfigurationChangeListener, // 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); + Bubble b = mBubbleData.getOrCreateBubble(pendingIntent, user); + ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - pendingIntent=%s", + pendingIntent); if (b.isInflated()) { mBubbleData.setSelectedBubbleAndExpandStack(b); } else { @@ -2756,13 +2792,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 +3019,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..f97133a4c3d1 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 @@ -471,6 +471,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); 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/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/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 3c7780711a14..7491abd4248b 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 } @@ -3074,6 +3086,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 +3095,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/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/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..15ac03ccaf30 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) @@ -3395,12 +3395,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 +3445,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 +3802,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 +3812,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 +3830,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..c92e67f1a0c0 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 @@ -473,7 +473,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 +486,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) @@ -658,6 +658,7 @@ class HandleMenu( this.taskInfo = this@HandleMenuView.taskInfo } appNameView.setTextColor(style.textColor) + appNameView.startMarquee() } private fun bindWindowingPill(style: MenuStyle) { @@ -693,11 +694,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 +717,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..4762bc21d79c 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 @@ -93,6 +93,9 @@ class AppHeaderViewHolder( private val lightColors = dynamicLightColorScheme(context) private val darkColors = dynamicDarkColorScheme(context) + private val headerButtonOpenMenuA11yText = context.resources + .getString(R.string.desktop_mode_app_header_chip_text) + /** * The corner radius to apply to the app chip, maximize and close button's background drawable. **/ @@ -228,6 +231,18 @@ class AppHeaderViewHolder( } } + val a11yActionOpenHeaderMenu = AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, + headerButtonOpenMenuA11yText) + openMenuButton.accessibilityDelegate = object : View.AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfo + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + info.addAction(a11yActionOpenHeaderMenu) + } + } + with(context.resources) { // Update a11y read out to say "double tap to maximize or restore window size" ViewCompat.replaceAccessibilityAction( @@ -260,6 +275,7 @@ class AppHeaderViewHolder( /** Sets the app's name in the header. */ fun setAppName(name: CharSequence) { appNameTextView.text = name + openMenuButton.contentDescription = 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/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..98c5bc960c35 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 @@ -292,7 +292,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)) @@ -5427,38 +5427,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 - fun onUnhandledDrag_newFreeformIntentSplitLeft() { + @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 + @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 - fun onUnhandledDrag_newFullscreenIntent() { + @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 + @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 +5864,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() indicatorType: DesktopModeVisualIndicator.IndicatorType, inputCoordinate: PointF, expectedBounds: Rect, + tabTearingAnimationFlagEnabled: Boolean, ) { setUpLandscapeDisplay() val task = setUpFreeformTask() @@ -5842,6 +5895,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, @@ -5858,8 +5921,19 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() verify(transitions).startTransition(any(), capture(arg), 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), + capture(arg), + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } else { + // All other launches use a special handler. + verify(dragAndDropTransitionHandler).handleDropEvent(capture(arg)) + } } assertThat( ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions) 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..7cd46af9402b 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 @@ -27,6 +27,8 @@ 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]. */ @@ -58,15 +60,14 @@ class DragZoneFactoryTest { DragZoneFactory(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 @@ -75,19 +76,18 @@ class DragZoneFactoryTest { DragZoneFactory(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, + 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>(), ) - dragZones.zip(expectedZones).forEach { (zone, expectedType) -> - assertThat(zone).isInstanceOf(expectedType) - } + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } } @Test @@ -95,19 +95,18 @@ class DragZoneFactoryTest { dragZoneFactory = DragZoneFactory(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, + 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>(), ) - dragZones.zip(expectedZones).forEach { (zone, expectedType) -> - assertThat(zone).isInstanceOf(expectedType) - } + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } } @Test @@ -115,18 +114,17 @@ class DragZoneFactoryTest { dragZoneFactory = DragZoneFactory(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, + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.Split.Left>(), + verifyInstance<DragZone.Split.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 @@ -134,18 +132,17 @@ class DragZoneFactoryTest { dragZoneFactory = DragZoneFactory(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, + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.Split.Top>(), + verifyInstance<DragZone.Split.Bottom>(), ) - dragZones.zip(expectedZones).forEach { (zone, expectedType) -> - assertThat(zone).isInstanceOf(expectedType) - } + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } } @Test @@ -156,19 +153,18 @@ class DragZoneFactoryTest { 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, + 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>(), ) - dragZones.zip(expectedZones).forEach { (zone, expectedType) -> - assertThat(zone).isInstanceOf(expectedType) - } + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } } @Test @@ -176,19 +172,18 @@ class DragZoneFactoryTest { dragZoneFactory = DragZoneFactory(tabletLandscape, 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.Left::class.java, - DragZone.Split.Right::class.java, - DragZone.Bubble.Left::class.java, - DragZone.Bubble.Right::class.java, + 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>(), ) - dragZones.zip(expectedZones).forEach { (zone, expectedType) -> - assertThat(zone).isInstanceOf(expectedType) - } + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } } @Test @@ -196,18 +191,17 @@ class DragZoneFactoryTest { dragZoneFactory = DragZoneFactory(foldablePortrait, 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.Split.Left::class.java, - DragZone.Split.Right::class.java, - DragZone.Bubble.Left::class.java, - DragZone.Bubble.Right::class.java, + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.Split.Left>(), + verifyInstance<DragZone.Split.Right>(), + 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 @@ -215,18 +209,17 @@ class DragZoneFactoryTest { dragZoneFactory = DragZoneFactory(foldableLandscape, 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.Split.Top::class.java, - DragZone.Split.Bottom::class.java, - DragZone.Bubble.Left::class.java, - DragZone.Bubble.Right::class.java, + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.Split.Top>(), + verifyInstance<DragZone.Split.Bottom>(), + 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 @@ -246,4 +239,8 @@ class DragZoneFactoryTest { 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/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/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/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/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/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..2ca70558f18b 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 @@ -207,12 +207,7 @@ fun CommunalContainer( 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..9c57efc24a22 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 @@ -1724,7 +1724,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/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/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/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/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt index dbdd7fb2773a..85155157eda2 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 @@ -203,7 +203,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 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/ui/viewmodel/LockscreenUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt index b66e2fe13e8a..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 @@ -41,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 @@ -275,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() } } 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/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/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/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt index a0d86f27b9b8..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,6 +28,7 @@ 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 @@ -60,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) @@ -713,4 +718,43 @@ class SceneInteractorTest : SysuiTestCase() { 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/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/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/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt index 816df0102940..403ac3288128 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) @@ -318,7 +296,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { fun chipsLegacy_twoTimerChips_isSmallPortrait_andChipsModernizationDisabled_bothSquished() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call")) + addOngoingCallState(key = "call") val latest by collectLastValue(underTest.chipsLegacy) @@ -334,7 +312,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { 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) @@ -354,7 +332,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 +341,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) @@ -387,7 +365,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { 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 = @@ -410,7 +388,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { 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) @@ -429,16 +407,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { fun chips_twoChips_chipsModernizationEnabled_notSquished() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording - setNotifs( - listOf( - activeNotificationModel( - key = "call", - statusBarChipIcon = createStatusBarIconViewOrNull(), - callType = CallType.Ongoing, - whenTime = 499, - ) - ) - ) + addOngoingCallState(key = "call") val latest by collectLastValue(underTest.chips) @@ -455,7 +424,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 +438,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 +479,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 +494,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 +512,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 +533,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 +549,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 +568,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 +782,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 +817,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 +852,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 +877,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 +911,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 +920,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 +951,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 +979,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 +992,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 +1013,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 +1044,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 +1091,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 +1277,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 +1304,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/unfold/DisplaySwitchLatencyTrackerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt index fecf1fd2f222..354edac75452 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,8 +47,10 @@ 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 @@ -56,11 +61,13 @@ 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 @@ -73,6 +80,7 @@ import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations import org.mockito.kotlin.mock +import org.mockito.kotlin.times @RunWith(AndroidJUnit4::class) @SmallTest @@ -88,6 +96,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 +151,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { displaySwitchLatencyLogger, systemClock, deviceStateManager, + latencyTracker, ) } @@ -195,6 +205,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { displaySwitchLatencyLogger, systemClock, deviceStateManager, + latencyTracker, ) displaySwitchLatencyTracker.start() @@ -370,6 +381,256 @@ 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) + } + } + + private suspend fun TestScope.startInFoldedState(tracker: DisplaySwitchLatencyTracker) { + setDeviceState(FOLDED) + 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/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/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/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 5ef4d4014ba6..7f2c89346423 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -258,7 +258,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/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/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/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/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/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/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/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/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/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/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/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt index 7a32491c0b67..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 @@ -240,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 } @@ -249,6 +255,7 @@ constructor( logger.logSceneChanged( from = currentSceneKey, to = resolvedScene, + sceneState = sceneState, reason = loggingReason, isInstant = false, ) @@ -272,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, ) @@ -489,11 +503,12 @@ 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) ) { @@ -503,6 +518,10 @@ constructor( "Can't change scene to ${to.debugName} in split shade mode!" } + if (from == to) { + return false + } + if (to !in repository.allContentKeys) { 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/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/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/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/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/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/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 a339bc98457e..58326dbb3a34 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java @@ -61,6 +61,7 @@ import kotlinx.coroutines.flow.StateFlowKt; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Objects; /** * The header group on Keyguard. @@ -103,6 +104,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 @@ -284,9 +288,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/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/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/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..cd401d5deb6e 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,9 +196,20 @@ 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() { @@ -165,12 +220,13 @@ constructor( 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 +261,7 @@ constructor( private fun DisplaySwitchLatencyEvent.withAfterFields( toFoldableDeviceState: Int, displaySwitchTimeMs: Int, - toState: Int + toState: Int, ): DisplaySwitchLatencyEvent { log { "toFoldableDeviceState=$toFoldableDeviceState, " + @@ -217,7 +273,7 @@ constructor( return copy( toFoldableDeviceState = toFoldableDeviceState, latencyMs = displaySwitchTimeMs, - toState = toState + toState = toState, ) } @@ -250,14 +306,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..6ac0bb168f18 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/unfold/NoCooldownDisplaySwitchLatencyTracker.kt @@ -0,0 +1,243 @@ +/* + * 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.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()") { + powerInteractor.screenPowerState.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/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/window/data/repository/WindowRootViewBlurRepository.kt b/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt index 6b7de982e00a..22a74c86e0f1 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,10 +16,8 @@ package com.android.systemui.window.data.repository -import android.annotation.SuppressLint import com.android.systemui.dagger.SysUISingleton import javax.inject.Inject -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow /** Repository that maintains state for the window blur effect. */ @@ -28,6 +26,4 @@ class WindowRootViewBlurRepository @Inject constructor() { val blurRadius = MutableStateFlow(0) val isBlurOpaque = MutableStateFlow(false) - - @SuppressLint("SharedFlowCreation") val onBlurApplied = MutableSharedFlow<Int>() } 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..9e369347dea5 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,7 +72,7 @@ constructor( * root view. */ suspend fun onBlurApplied(appliedBlurRadius: Int) { - repository.onBlurApplied.emit(appliedBlurRadius) + _onBlurAppliedEvent.emit(appliedBlurRadius) } /** Radius of blur to be applied on the window root view. */ @@ -77,7 +81,7 @@ constructor( /** * 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/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/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/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/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/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/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/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/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/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/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/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 2219ecc77167..6f79f7073b89 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"); - } - } } } } 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/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/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/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/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..1c7b2d3284d9 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -5239,7 +5239,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(), 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/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..1dd7c4d4adbd 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -6635,6 +6635,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 */ 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/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..d5626661725e 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -732,8 +732,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; 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/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/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/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/wallpaper/TestWallpaperService.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/TestWallpaperService.java new file mode 100644 index 000000000000..85ea5a0f2c2e --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/TestWallpaperService.java @@ -0,0 +1,26 @@ +/* + * 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.wallpaper; + +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/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/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/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 4718fbf085f8..ff4d8ef2ec25 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -2505,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/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/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. |