diff options
304 files changed, 5665 insertions, 2635 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index d610f4c8d4ed..c5a70df0905e 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -34161,6 +34161,7 @@ package android.os { field public static final int USAGE_CLASS_UNKNOWN = 0; // 0x0 field public static final int USAGE_COMMUNICATION_REQUEST = 65; // 0x41 field public static final int USAGE_HARDWARE_FEEDBACK = 50; // 0x32 + field @FlaggedApi("android.os.vibrator.vibration_attribute_ime_usage_api") public static final int USAGE_IME_FEEDBACK = 82; // 0x52 field public static final int USAGE_MEDIA = 19; // 0x13 field public static final int USAGE_NOTIFICATION = 49; // 0x31 field public static final int USAGE_PHYSICAL_EMULATION = 34; // 0x22 diff --git a/core/java/android/app/Person.java b/core/java/android/app/Person.java index 96f6f4eac372..c7432c571e43 100644 --- a/core/java/android/app/Person.java +++ b/core/java/android/app/Person.java @@ -189,10 +189,8 @@ public final class Person implements Parcelable { */ public void visitUris(@NonNull Consumer<Uri> visitor) { visitor.accept(getIconUri()); - if (Flags.visitPersonUri()) { - if (mUri != null && !mUri.isEmpty()) { - visitor.accept(Uri.parse(mUri)); - } + if (mUri != null && !mUri.isEmpty()) { + visitor.accept(Uri.parse(mUri)); } } diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig index 19de7936982c..a1ae9da636d5 100644 --- a/core/java/android/companion/virtual/flags/flags.aconfig +++ b/core/java/android/companion/virtual/flags/flags.aconfig @@ -84,6 +84,13 @@ flag { } flag { + namespace: "virtual_devices" + name: "enforce_remote_device_opt_out_on_all_virtual_displays" + description: "Respect canDisplayOnRemoteDevices on all virtual displays" + bug: "338973239" +} + +flag { namespace: "virtual_devices" name: "virtual_display_multi_window_mode_support" description: "Add support for WINDOWING_MODE_MULTI_WINDOW to virtual displays by default" diff --git a/core/java/android/content/IntentSender.java b/core/java/android/content/IntentSender.java index 32d1964dd4f0..ca6d86ae2dd8 100644 --- a/core/java/android/content/IntentSender.java +++ b/core/java/android/content/IntentSender.java @@ -16,6 +16,8 @@ package android.content; +import static android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM; + import android.annotation.FlaggedApi; import android.annotation.Nullable; import android.app.ActivityManager; @@ -23,6 +25,9 @@ import android.app.ActivityManager.PendingIntentInfo; import android.app.ActivityOptions; import android.app.ActivityThread; import android.app.IApplicationThread; +import android.app.compat.CompatChanges; +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledAfter; import android.compat.annotation.UnsupportedAppUsage; import android.os.Bundle; import android.os.Handler; @@ -65,6 +70,11 @@ import java.util.concurrent.Executor; * {@link android.app.PendingIntent#getIntentSender() PendingIntent.getIntentSender()}. */ public class IntentSender implements Parcelable { + /** If enabled consider the deprecated @hide method as removed. */ + @ChangeId + @EnabledAfter(targetSdkVersion = VANILLA_ICE_CREAM) + private static final long REMOVE_HIDDEN_SEND_INTENT_METHOD = 356174596; + private static final Bundle SEND_INTENT_DEFAULT_OPTIONS = ActivityOptions.makeBasic().setPendingIntentBackgroundActivityStartMode( ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_COMPAT).toBundle(); @@ -220,6 +230,44 @@ public class IntentSender implements Parcelable { * original Intent. Use {@code null} to not modify the original Intent. * @param onFinished The object to call back on when the send has * completed, or {@code null} for no callback. + * @param handler Handler identifying the thread on which the callback + * should happen. If {@code null}, the callback will happen from the thread + * pool of the process. + * @param options Additional options the caller would like to provide to modify the sending + * behavior. Typically built from using {@link ActivityOptions} to apply to an activity start. + * + * @throws SendIntentException Throws CanceledIntentException if the IntentSender + * is no longer allowing more intents to be sent through it. + * + * @deprecated use {@link #sendIntent(Context, int, Intent, String, Bundle, Executor, + * OnFinished)} + * + * @hide + */ + @Deprecated public void sendIntent(Context context, int code, Intent intent, + OnFinished onFinished, Handler handler, String requiredPermission, + @Nullable Bundle options) + throws SendIntentException { + if (CompatChanges.isChangeEnabled(REMOVE_HIDDEN_SEND_INTENT_METHOD)) { + throw new NoSuchMethodError("This overload of sendIntent was removed."); + } + sendIntent(context, code, intent, requiredPermission, options, + handler == null ? null : handler::post, onFinished); + } + + /** + * Perform the operation associated with this IntentSender, allowing the + * caller to specify information about the Intent to use and be notified + * when the send has completed. + * + * @param context The Context of the caller. This may be {@code null} if + * <var>intent</var> is also {@code null}. + * @param code Result code to supply back to the IntentSender's target. + * @param intent Additional Intent data. See {@link Intent#fillIn + * Intent.fillIn()} for information on how this is applied to the + * original Intent. Use {@code null} to not modify the original Intent. + * @param onFinished The object to call back on when the send has + * completed, or {@code null} for no callback. * @param executor Executor identifying the thread on which the callback * should happen. If {@code null}, the callback will happen from the thread * pool of the process. diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index 6952a09f2d90..481e6b530162 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -617,7 +617,7 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { */ public static final int FLAG_ENABLE_VR_MODE = 0x8000; /** - * Bit in {@link #flags} indicating if the activity can be displayed on a remote device. + * Bit in {@link #flags} indicating if the activity can be displayed on a virtual display. * Corresponds to {@link android.R.attr#canDisplayOnRemoteDevices} * @hide */ diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index 88fbbddd28c9..6882d5c2db00 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -359,3 +359,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "show_different_creation_error_for_unsupported_devices" + namespace: "profile_experiences" + description: "On private space create error due to child account added/fully managed user show message with link to the Help Center to find out more." + bug: "340130375" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index fe14d4570e9e..00ce9491784b 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -1171,12 +1171,14 @@ public class InputMethodService extends AbstractInputMethodService { } switch (motionEvent.getAction()) { case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_HOVER_ENTER: // Consume and ignore all touches while stylus is down to prevent // accidental touches from going to the app while writing. mPrivOps.setHandwritingSurfaceNotTouchable(false); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_HOVER_EXIT: // Go back to only consuming stylus events so that the user // can continue to interact with the app using touch // when the stylus is not down. diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index 136c45d1695f..47096dbbac61 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -434,7 +434,6 @@ public final class Parcel { @RavenwoodThrow private static native void nativeWriteStrongBinder(long nativePtr, IBinder val); @FastNative - @RavenwoodThrow private static native void nativeWriteFileDescriptor(long nativePtr, FileDescriptor val); private static native byte[] nativeCreateByteArray(long nativePtr); @@ -456,7 +455,6 @@ public final class Parcel { @RavenwoodThrow private static native IBinder nativeReadStrongBinder(long nativePtr); @FastNative - @RavenwoodThrow private static native FileDescriptor nativeReadFileDescriptor(long nativePtr); private static native long nativeCreate(); diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java index 71957ee3461e..464df239b8fd 100644 --- a/core/java/android/os/ParcelFileDescriptor.java +++ b/core/java/android/os/ParcelFileDescriptor.java @@ -381,6 +381,8 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { } private static void closeInternal$ravenwood(FileDescriptor fd) { + // Desktop JVM doesn't have FileDescriptor.close(), so we'll need to go to the ravenwood + // side to close it. native_close$ravenwood(fd); } diff --git a/core/java/android/os/PowerManagerInternal.java b/core/java/android/os/PowerManagerInternal.java index ce3156e4816f..1fb7937ff847 100644 --- a/core/java/android/os/PowerManagerInternal.java +++ b/core/java/android/os/PowerManagerInternal.java @@ -139,11 +139,16 @@ public abstract class PowerManagerInternal { * @param screenState The overridden screen state, or {@link Display#STATE_UNKNOWN} * to disable the override. * @param reason The reason for overriding the screen state. - * @param screenBrightness The overridden screen brightness, or - * {@link PowerManager#BRIGHTNESS_DEFAULT} to disable the override. + * @param screenBrightnessFloat The overridden screen brightness between + * {@link PowerManager#BRIGHTNESS_MIN} and {@link PowerManager#BRIGHTNESS_MAX}, or + * {@link PowerManager#BRIGHTNESS_INVALID_FLOAT} if screenBrightnessInt should be used instead. + * @param screenBrightnessInt The overridden screen brightness between 1 and 255, or + * {@link PowerManager#BRIGHTNESS_DEFAULT} to disable the override. Not used if + * screenBrightnessFloat is provided (is not NaN). */ public abstract void setDozeOverrideFromDreamManager( - int screenState, @Display.StateReason int reason, int screenBrightness); + int screenState, @Display.StateReason int reason, float screenBrightnessFloat, + int screenBrightnessInt); /** * Used by sidekick manager to tell the power manager if it shouldn't change the display state diff --git a/core/java/android/os/VibrationAttributes.java b/core/java/android/os/VibrationAttributes.java index 9df5b850188f..da863e58a882 100644 --- a/core/java/android/os/VibrationAttributes.java +++ b/core/java/android/os/VibrationAttributes.java @@ -16,6 +16,9 @@ package android.os; +import static android.os.vibrator.Flags.FLAG_VIBRATION_ATTRIBUTE_IME_USAGE_API; + +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -55,6 +58,7 @@ public final class VibrationAttributes implements Parcelable { USAGE_PHYSICAL_EMULATION, USAGE_RINGTONE, USAGE_TOUCH, + USAGE_IME_FEEDBACK, }) @Retention(RetentionPolicy.SOURCE) public @interface Usage {} @@ -136,6 +140,12 @@ public final class VibrationAttributes implements Parcelable { */ public static final int USAGE_ACCESSIBILITY = 0x40 | USAGE_CLASS_FEEDBACK; /** + * Usage value to use for input method editor (IME) haptic feedback. + */ + @FlaggedApi(FLAG_VIBRATION_ATTRIBUTE_IME_USAGE_API) + public static final int USAGE_IME_FEEDBACK = 0x50 | USAGE_CLASS_FEEDBACK; + + /** * Usage value to use for media vibrations, such as music, movie, soundtrack, animations, games, * or any interactive media that isn't for touch feedback specifically. */ @@ -174,7 +184,6 @@ public final class VibrationAttributes implements Parcelable { FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF, FLAG_INVALIDATE_SETTINGS_CACHE, FLAG_PIPELINED_EFFECT, - FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE }) @Retention(RetentionPolicy.SOURCE) public @interface Flag{} @@ -228,31 +237,12 @@ public final class VibrationAttributes implements Parcelable { public static final int FLAG_PIPELINED_EFFECT = 1 << 3; /** - * Flag requesting that this vibration effect to be played without applying the user - * intensity setting to scale the vibration. - * - * <p>The user setting is still applied to enable/disable the vibration, but the vibration - * effect strength will not be scaled based on the enabled setting value. - * - * <p>This is intended to be used on scenarios where the system needs to enforce a specific - * strength for the vibration effect, regardless of the user preference. Only privileged apps - * can ignore user settings, and this flag will be ignored otherwise. - * - * <p>If you need to bypass the user setting when it's disabling vibrations then this also - * needs the flag {@link #FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF} to be set. - * - * @hide - */ - public static final int FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE = 1 << 4; - - /** * All flags supported by vibrator service, update it when adding new flag. * @hide */ public static final int FLAG_ALL_SUPPORTED = FLAG_BYPASS_INTERRUPTION_POLICY | FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF - | FLAG_INVALIDATE_SETTINGS_CACHE | FLAG_PIPELINED_EFFECT - | FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE; + | FLAG_INVALIDATE_SETTINGS_CACHE | FLAG_PIPELINED_EFFECT; /** Creates a new {@link VibrationAttributes} instance with given usage. */ public static @NonNull VibrationAttributes createForUsage(@Usage int usage) { @@ -349,6 +339,7 @@ public final class VibrationAttributes implements Parcelable { case USAGE_RINGTONE: return AudioAttributes.USAGE_NOTIFICATION_RINGTONE; case USAGE_TOUCH: + case USAGE_IME_FEEDBACK: return AudioAttributes.USAGE_ASSISTANCE_SONIFICATION; case USAGE_ALARM: return AudioAttributes.USAGE_ALARM; @@ -447,6 +438,8 @@ public final class VibrationAttributes implements Parcelable { return "PHYSICAL_EMULATION"; case USAGE_HARDWARE_FEEDBACK: return "HARDWARE_FEEDBACK"; + case USAGE_IME_FEEDBACK: + return "IME"; default: return "unknown usage " + usage; } diff --git a/core/java/android/os/vibrator/VibrationConfig.java b/core/java/android/os/vibrator/VibrationConfig.java index f6e73b39f84b..a4164e9f204c 100644 --- a/core/java/android/os/vibrator/VibrationConfig.java +++ b/core/java/android/os/vibrator/VibrationConfig.java @@ -20,6 +20,7 @@ import static android.os.VibrationAttributes.USAGE_ACCESSIBILITY; import static android.os.VibrationAttributes.USAGE_ALARM; import static android.os.VibrationAttributes.USAGE_COMMUNICATION_REQUEST; import static android.os.VibrationAttributes.USAGE_HARDWARE_FEEDBACK; +import static android.os.VibrationAttributes.USAGE_IME_FEEDBACK; import static android.os.VibrationAttributes.USAGE_MEDIA; import static android.os.VibrationAttributes.USAGE_NOTIFICATION; import static android.os.VibrationAttributes.USAGE_PHYSICAL_EMULATION; @@ -67,6 +68,8 @@ public class VibrationConfig { private final int mDefaultNotificationVibrationIntensity; @VibrationIntensity private final int mDefaultRingVibrationIntensity; + @VibrationIntensity + private final int mDefaultKeyboardVibrationIntensity; private final boolean mKeyboardVibrationSettingsSupported; @@ -98,6 +101,8 @@ public class VibrationConfig { com.android.internal.R.integer.config_defaultNotificationVibrationIntensity); mDefaultRingVibrationIntensity = loadDefaultIntensity(resources, com.android.internal.R.integer.config_defaultRingVibrationIntensity); + mDefaultKeyboardVibrationIntensity = loadDefaultIntensity(resources, + com.android.internal.R.integer.config_defaultKeyboardVibrationIntensity); } @VibrationIntensity @@ -213,6 +218,9 @@ public class VibrationConfig { case USAGE_PHYSICAL_EMULATION: case USAGE_ACCESSIBILITY: return mDefaultHapticFeedbackIntensity; + case USAGE_IME_FEEDBACK: + return isKeyboardVibrationSettingsSupported() + ? mDefaultKeyboardVibrationIntensity : mDefaultHapticFeedbackIntensity; case USAGE_MEDIA: case USAGE_UNKNOWN: // fall through @@ -236,6 +244,7 @@ public class VibrationConfig { + ", mDefaultMediaIntensity=" + mDefaultMediaVibrationIntensity + ", mDefaultNotificationIntensity=" + mDefaultNotificationVibrationIntensity + ", mDefaultRingIntensity=" + mDefaultRingVibrationIntensity + + ", mDefaultKeyboardIntensity=" + mDefaultKeyboardVibrationIntensity + ", mKeyboardVibrationSettingsSupported=" + mKeyboardVibrationSettingsSupported + "}"; } diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig index 62b3682d0bb3..67c346401804 100644 --- a/core/java/android/os/vibrator/flags.aconfig +++ b/core/java/android/os/vibrator/flags.aconfig @@ -74,3 +74,14 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + namespace: "haptics" + name: "vibration_attribute_ime_usage_api" + is_exported: true + description: "A public API for IME usage vibration attribute" + bug: "332661766" + metadata { + purpose: PURPOSE_FEATURE + } +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index c18653811f13..0ee6f43e1329 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -15820,7 +15820,7 @@ public final class Settings { * The following keys are supported: * * <pre> - * screen_brightness_array (int[]) + * screen_brightness_array (int[], values in range [1, 255]) * dimming_scrim_array (int[]) * prox_screen_off_delay (long) * prox_cooldown_trigger (long) diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java index 74545a8b25ff..06e53ac8e7a2 100644 --- a/core/java/android/service/dreams/DreamService.java +++ b/core/java/android/service/dreams/DreamService.java @@ -74,6 +74,7 @@ import android.view.accessibility.AccessibilityEvent; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.display.BrightnessSynchronizer; import com.android.internal.util.DumpUtils; import java.io.FileDescriptor; @@ -269,6 +270,7 @@ public class DreamService extends Service implements Window.Callback { private volatile int mDozeScreenState = Display.STATE_UNKNOWN; private volatile @Display.StateReason int mDozeScreenStateReason = Display.STATE_REASON_UNKNOWN; private volatile int mDozeScreenBrightness = PowerManager.BRIGHTNESS_DEFAULT; + private volatile float mDozeScreenBrightnessFloat = PowerManager.BRIGHTNESS_INVALID_FLOAT; private boolean mDebug = false; @@ -927,12 +929,12 @@ public class DreamService extends Service implements Window.Callback { try { if (startAndStopDozingInBackground()) { mDreamManager.startDozingOneway( - mDreamToken, mDozeScreenState, mDozeScreenStateReason, - mDozeScreenBrightness); + mDreamToken, mDozeScreenState, mDozeScreenStateReason, + mDozeScreenBrightnessFloat, mDozeScreenBrightness); } else { mDreamManager.startDozing( mDreamToken, mDozeScreenState, mDozeScreenStateReason, - mDozeScreenBrightness); + mDozeScreenBrightnessFloat, mDozeScreenBrightness); } } catch (RemoteException ex) { @@ -1057,7 +1059,7 @@ public class DreamService extends Service implements Window.Callback { * Gets the screen brightness to use while dozing. * * @return The screen brightness while dozing as a value between - * {@link PowerManager#BRIGHTNESS_OFF} (0) and {@link PowerManager#BRIGHTNESS_ON} (255), + * {@link PowerManager#BRIGHTNESS_OFF + 1} (1) and {@link PowerManager#BRIGHTNESS_ON} (255), * or {@link PowerManager#BRIGHTNESS_DEFAULT} (-1) to ask the system to apply * its default policy based on the screen state. * @@ -1078,11 +1080,11 @@ public class DreamService extends Service implements Window.Callback { * The dream may set a different brightness before starting to doze and may adjust * the brightness while dozing to conserve power and achieve various effects. * </p><p> - * Note that dream may specify any brightness in the full 0-255 range, including + * Note that dream may specify any brightness in the full 1-255 range, including * values that are less than the minimum value for manual screen brightness - * adjustments by the user. In particular, the value may be set to 0 which may - * turn off the backlight entirely while still leaving the screen on although - * this behavior is device dependent and not guaranteed. + * adjustments by the user. In particular, the value may be set to + * {@link PowerManager.BRIGHTNESS_OFF} which may turn off the backlight entirely while still + * leaving the screen on although this behavior is device dependent and not guaranteed. * </p><p> * The available range of display brightness values and their behavior while dozing is * hardware dependent and may vary across devices. The dream may therefore @@ -1090,7 +1092,7 @@ public class DreamService extends Service implements Window.Callback { * </p> * * @param brightness The screen brightness while dozing as a value between - * {@link PowerManager#BRIGHTNESS_OFF} (0) and {@link PowerManager#BRIGHTNESS_ON} (255), + * {@link PowerManager#BRIGHTNESS_OFF + 1} (1) and {@link PowerManager#BRIGHTNESS_ON} (255), * or {@link PowerManager#BRIGHTNESS_DEFAULT} (-1) to ask the system to apply * its default policy based on the screen state. * @@ -1108,6 +1110,44 @@ public class DreamService extends Service implements Window.Callback { } /** + * Sets the screen brightness to use while dozing. + * <p> + * The value of this property determines the power state of the primary display + * once {@link #startDozing} has been called. The default value is + * {@link PowerManager#BRIGHTNESS_INVALID_FLOAT} which lets the system decide. + * The dream may set a different brightness before starting to doze and may adjust + * the brightness while dozing to conserve power and achieve various effects. + * </p><p> + * Note that dream may specify any brightness in the full 0-1 range, including + * values that are less than the minimum value for manual screen brightness + * adjustments by the user. In particular, the value may be set to + * {@link PowerManager#BRIGHTNESS_OFF_FLOAT} which may turn off the backlight entirely while + * still leaving the screen on although this behavior is device dependent and not guaranteed. + * </p><p> + * The available range of display brightness values and their behavior while dozing is + * hardware dependent and may vary across devices. The dream may therefore + * need to be modified or configured to correctly support the hardware. + * </p> + * + * @param brightness The screen brightness while dozing as a value between + * {@link PowerManager#BRIGHTNESS_MIN} (0) and {@link PowerManager#BRIGHTNESS_MAX} (1), + * or {@link PowerManager#BRIGHTNESS_INVALID_FLOAT} (Float.NaN) to ask the system to apply + * its default policy based on the screen state. + * + * @hide For use by system UI components only. + */ + @UnsupportedAppUsage + public void setDozeScreenBrightnessFloat(float brightness) { + if (!Float.isNaN(brightness)) { + brightness = clampAbsoluteBrightnessFloat(brightness); + } + if (!BrightnessSynchronizer.floatEquals(mDozeScreenBrightnessFloat, brightness)) { + mDozeScreenBrightnessFloat = brightness; + updateDoze(); + } + } + + /** * Called when this Dream is constructed. */ @Override @@ -1751,6 +1791,13 @@ public class DreamService extends Service implements Window.Callback { return MathUtils.constrain(value, PowerManager.BRIGHTNESS_OFF, PowerManager.BRIGHTNESS_ON); } + private static float clampAbsoluteBrightnessFloat(float value) { + if (value == PowerManager.BRIGHTNESS_OFF_FLOAT) { + return value; + } + return MathUtils.constrain(value, PowerManager.BRIGHTNESS_MIN, PowerManager.BRIGHTNESS_MAX); + } + /** * The DreamServiceWrapper is used as a gateway to the system_server, where DreamController * uses it to control the DreamService. It is also used to receive callbacks from the diff --git a/core/java/android/service/dreams/IDreamManager.aidl b/core/java/android/service/dreams/IDreamManager.aidl index 76f63631e76a..611e7912517b 100644 --- a/core/java/android/service/dreams/IDreamManager.aidl +++ b/core/java/android/service/dreams/IDreamManager.aidl @@ -42,7 +42,8 @@ interface IDreamManager { /** @deprecated Please use finishSelfOneway instead. */ void finishSelf(in IBinder token, boolean immediate); /** @deprecated Please use startDozingOneway instead. */ - void startDozing(in IBinder token, int screenState, int reason, int screenBrightness); + void startDozing(in IBinder token, int screenState, int reason, float screenBrightnessFloat, + int screenBrightnessInt); void stopDozing(in IBinder token); void forceAmbientDisplayEnabled(boolean enabled); ComponentName[] getDreamComponentsForUser(int userId); @@ -52,6 +53,7 @@ interface IDreamManager { void startDreamActivity(in Intent intent); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.WRITE_DREAM_STATE)") oneway void setDreamIsObscured(in boolean isObscured); - oneway void startDozingOneway(in IBinder token, int screenState, int reason, int screenBrightness); + oneway void startDozingOneway(in IBinder token, int screenState, int reason, + float screenBrightnessFloat, int screenBrightnessInt); oneway void finishSelfOneway(in IBinder token, boolean immediate); } diff --git a/core/java/android/view/InputEventAssigner.java b/core/java/android/view/InputEventAssigner.java index 7fac6c5e4af6..30d9aaa4b144 100644 --- a/core/java/android/view/InputEventAssigner.java +++ b/core/java/android/view/InputEventAssigner.java @@ -17,7 +17,8 @@ package android.view; import static android.os.IInputConstants.INVALID_INPUT_EVENT_ID; -import static android.view.InputDevice.SOURCE_TOUCHSCREEN; +import static android.view.InputDevice.SOURCE_CLASS_POINTER; +import static android.view.InputDevice.SOURCE_CLASS_POSITION; /** * Process input events and assign input event id to a specific frame. @@ -64,18 +65,19 @@ public class InputEventAssigner { public int processEvent(InputEvent event) { if (event instanceof MotionEvent) { MotionEvent motionEvent = (MotionEvent) event; - if (motionEvent.isFromSource(SOURCE_TOUCHSCREEN)) { + if (motionEvent.isFromSource(SOURCE_CLASS_POINTER) || motionEvent.isFromSource( + SOURCE_CLASS_POSITION)) { final int action = motionEvent.getActionMasked(); if (action == MotionEvent.ACTION_DOWN) { mHasUnprocessedDown = true; mDownEventId = event.getId(); } - if (mHasUnprocessedDown && action == MotionEvent.ACTION_MOVE) { - return mDownEventId; - } if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { mHasUnprocessedDown = false; } + if (mHasUnprocessedDown) { + return mDownEventId; + } } } return event.getId(); diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java index a4ca55ebf690..2f515fe7738c 100644 --- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java +++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java @@ -201,7 +201,9 @@ final class IInputMethodManagerGlobalInvoker { * @param exceptionHandler an optional {@link RemoteException} handler */ @AnyThread - @RequiresPermission(Manifest.permission.INTERNAL_SYSTEM_WINDOW) + @RequiresPermission(allOf = { + Manifest.permission.INTERNAL_SYSTEM_WINDOW, + Manifest.permission.INTERACT_ACROSS_USERS_FULL}) static void removeImeSurface(int displayId, @Nullable Consumer<RemoteException> exceptionHandler) { final IInputMethodManager service = getService(); @@ -441,7 +443,9 @@ final class IInputMethodManagerGlobalInvoker { } @AnyThread - @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS) + @RequiresPermission(allOf = { + Manifest.permission.WRITE_SECURE_SETTINGS, + Manifest.permission.INTERACT_ACROSS_USERS_FULL}) static void showInputMethodPickerFromSystem(int auxiliarySubtypeMode, int displayId) { final IInputMethodManager service = getService(); if (service == null) { @@ -469,7 +473,9 @@ final class IInputMethodManagerGlobalInvoker { } @AnyThread - @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS) + @RequiresPermission(allOf = { + Manifest.permission.WRITE_SECURE_SETTINGS, + Manifest.permission.INTERACT_ACROSS_USERS_FULL}) static void onImeSwitchButtonClickFromSystem(int displayId) { final IInputMethodManager service = getService(); if (service == null) { diff --git a/core/java/android/window/TaskFragmentInfo.java b/core/java/android/window/TaskFragmentInfo.java index fa5195727afe..23a1224fcc4e 100644 --- a/core/java/android/window/TaskFragmentInfo.java +++ b/core/java/android/window/TaskFragmentInfo.java @@ -102,6 +102,8 @@ public final class TaskFragmentInfo implements Parcelable { @NonNull private final Point mMinimumDimensions = new Point(); + private final boolean mIsTopNonFishingChild; + /** @hide */ public TaskFragmentInfo( @NonNull IBinder fragmentToken, @NonNull WindowContainerToken token, @@ -110,7 +112,7 @@ public final class TaskFragmentInfo implements Parcelable { @NonNull List<IBinder> inRequestedTaskFragmentActivities, @NonNull Point positionInParent, boolean isTaskClearedForReuse, boolean isTaskFragmentClearedForPip, boolean isClearedForReorderActivityToFront, - @NonNull Point minimumDimensions) { + @NonNull Point minimumDimensions, boolean isTopNonFinishingChild) { mFragmentToken = requireNonNull(fragmentToken); mToken = requireNonNull(token); mConfiguration.setTo(configuration); @@ -123,6 +125,7 @@ public final class TaskFragmentInfo implements Parcelable { mIsTaskFragmentClearedForPip = isTaskFragmentClearedForPip; mIsClearedForReorderActivityToFront = isClearedForReorderActivityToFront; mMinimumDimensions.set(minimumDimensions); + mIsTopNonFishingChild = isTopNonFinishingChild; } @NonNull @@ -212,6 +215,16 @@ public final class TaskFragmentInfo implements Parcelable { } /** + * Indicates that this TaskFragment is the top non-finishing child of its parent container + * among all Activities and TaskFragment siblings. + * + * @hide + */ + public boolean isTopNonFinishingChild() { + return mIsTopNonFishingChild; + } + + /** * Returns {@code true} if the parameters that are important for task fragment organizers are * equal between this {@link TaskFragmentInfo} and {@param that}. * Note that this method is usually called with @@ -236,7 +249,8 @@ public final class TaskFragmentInfo implements Parcelable { && mIsTaskClearedForReuse == that.mIsTaskClearedForReuse && mIsTaskFragmentClearedForPip == that.mIsTaskFragmentClearedForPip && mIsClearedForReorderActivityToFront == that.mIsClearedForReorderActivityToFront - && mMinimumDimensions.equals(that.mMinimumDimensions); + && mMinimumDimensions.equals(that.mMinimumDimensions) + && mIsTopNonFishingChild == that.mIsTopNonFishingChild; } private TaskFragmentInfo(Parcel in) { @@ -252,6 +266,7 @@ public final class TaskFragmentInfo implements Parcelable { mIsTaskFragmentClearedForPip = in.readBoolean(); mIsClearedForReorderActivityToFront = in.readBoolean(); mMinimumDimensions.readFromParcel(in); + mIsTopNonFishingChild = in.readBoolean(); } /** @hide */ @@ -269,6 +284,7 @@ public final class TaskFragmentInfo implements Parcelable { dest.writeBoolean(mIsTaskFragmentClearedForPip); dest.writeBoolean(mIsClearedForReorderActivityToFront); mMinimumDimensions.writeToParcel(dest, flags); + dest.writeBoolean(mIsTopNonFishingChild); } @NonNull @@ -299,6 +315,7 @@ public final class TaskFragmentInfo implements Parcelable { + " isTaskFragmentClearedForPip=" + mIsTaskFragmentClearedForPip + " mIsClearedForReorderActivityToFront=" + mIsClearedForReorderActivityToFront + " minimumDimensions=" + mMinimumDimensions + + " isTopNonFinishingChild=" + mIsTopNonFishingChild + "}"; } diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 725d49611fff..e5a9b6ac55c8 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -65,14 +65,6 @@ flag { } flag { - name: "defer_display_updates" - namespace: "windowing_frontend" - description: "Feature flag for deferring DisplayManager updates to WindowManager if Shell transition is running" - bug: "259220649" - is_fixed_read_only: true -} - -flag { name: "close_to_square_config_includes_status_bar" namespace: "windowing_frontend" description: "On close to square display, when necessary, configuration includes status bar" diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig index 4230641e2e12..4c18bbfbeebf 100644 --- a/core/java/android/window/flags/windowing_sdk.aconfig +++ b/core/java/android/window/flags/windowing_sdk.aconfig @@ -51,13 +51,6 @@ flag { flag { namespace: "windowing_sdk" - name: "embedded_activity_back_nav_flag" - description: "Refines embedded activity back navigation behavior" - bug: "293642394" -} - -flag { - namespace: "windowing_sdk" name: "cover_display_opt_in" is_exported: true description: "Properties to allow apps and activities to opt-in to cover display rendering" diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index ab456a84d9ad..6258f5ca721a 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -544,6 +544,14 @@ public class ChooserActivity extends ResolverActivity implements @Override protected void onCreate(Bundle savedInstanceState) { + if (Settings.Secure.getIntForUser(getContentResolver(), + Settings.Secure.SECURE_FRP_MODE, 0, + getUserId()) == 1) { + Log.e(TAG, "Sharing disabled due to active FRP lock."); + super.onCreate(savedInstanceState); + finish(); + return; + } final long intentReceivedTime = System.currentTimeMillis(); mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET); diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl index 3f7ba0aa69eb..b51678e82ed0 100644 --- a/core/java/com/android/internal/view/IInputMethodManager.aidl +++ b/core/java/com/android/internal/view/IInputMethodManager.aidl @@ -125,9 +125,9 @@ interface IInputMethodManager { void showInputMethodPickerFromClient(in IInputMethodClient client, int auxiliarySubtypeMode); - @EnforcePermission("WRITE_SECURE_SETTINGS") - @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " - + "android.Manifest.permission.WRITE_SECURE_SETTINGS)") + @EnforcePermission(allOf = {"WRITE_SECURE_SETTINGS", "INTERACT_ACROSS_USERS_FULL"}) + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf = {android.Manifest." + + "permission.WRITE_SECURE_SETTINGS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})") void showInputMethodPickerFromSystem(int auxiliarySubtypeMode, int displayId); @EnforcePermission("TEST_INPUT_METHOD") @@ -143,9 +143,9 @@ interface IInputMethodManager { * @param displayId The ID of the display where the input method picker dialog should be shown. * @param userId The ID of the user that triggered the click. */ - @EnforcePermission("WRITE_SECURE_SETTINGS") - @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " - + "android.Manifest.permission.WRITE_SECURE_SETTINGS)") + @EnforcePermission(allOf = {"WRITE_SECURE_SETTINGS" ,"INTERACT_ACROSS_USERS_FULL"}) + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf = {android.Manifest." + + "permission.WRITE_SECURE_SETTINGS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})") oneway void onImeSwitchButtonClickFromSystem(int displayId); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " @@ -168,9 +168,9 @@ interface IInputMethodManager { oneway void reportPerceptibleAsync(in IBinder windowToken, boolean perceptible); - @EnforcePermission("INTERNAL_SYSTEM_WINDOW") - @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " - + "android.Manifest.permission.INTERNAL_SYSTEM_WINDOW)") + @EnforcePermission(allOf = {"INTERNAL_SYSTEM_WINDOW", "INTERACT_ACROSS_USERS_FULL"}) + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf = {android.Manifest." + + "permission.INTERNAL_SYSTEM_WINDOW, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})") void removeImeSurface(int displayId); /** Remove the IME surface. Requires passing the currently focused window. */ diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp index 7c62615cdc42..638591f130ab 100644 --- a/core/jni/android_media_AudioSystem.cpp +++ b/core/jni/android_media_AudioSystem.cpp @@ -2292,7 +2292,7 @@ static jint nativeAudioMixToJavaAudioMixingRule(JNIEnv *env, const AudioMix &nAu criteria.mValue.mUsage); jMixMatchCriterion = env->NewObject(gAudioMixMatchCriterionClass, gAudioMixMatchCriterionAttrCstor, - jMixMatchCriterion, criteria.mRule); + jAudioAttributes, criteria.mRule); break; case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET: jAudioAttributes = env->NewObject(gAudioAttributesClass, gAudioAttributesCstor); @@ -2300,7 +2300,7 @@ static jint nativeAudioMixToJavaAudioMixingRule(JNIEnv *env, const AudioMix &nAu criteria.mValue.mSource); jMixMatchCriterion = env->NewObject(gAudioMixMatchCriterionClass, gAudioMixMatchCriterionAttrCstor, - jMixMatchCriterion, criteria.mRule); + jAudioAttributes, criteria.mRule); break; } env->CallBooleanMethod(jAudioMixMatchCriterionList, gArrayListMethods.add, diff --git a/core/res/res/drawable/tooltip_frame.xml b/core/res/res/drawable/tooltip_frame.xml index 14130c899e96..e2618cad1d15 100644 --- a/core/res/res/drawable/tooltip_frame.xml +++ b/core/res/res/drawable/tooltip_frame.xml @@ -17,5 +17,5 @@ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <solid android:color="?attr/tooltipBackgroundColor" /> - <corners android:radius="@dimen/tooltip_corner_radius" /> -</shape>
\ No newline at end of file + <corners android:radius="?attr/tooltipCornerRadius" /> +</shape> diff --git a/core/res/res/layout/tooltip.xml b/core/res/res/layout/tooltip.xml index 376c5eb125f4..5b6799e23f85 100644 --- a/core/res/res/layout/tooltip.xml +++ b/core/res/res/layout/tooltip.xml @@ -27,10 +27,10 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="@dimen/tooltip_margin" - android:paddingStart="@dimen/tooltip_horizontal_padding" - android:paddingEnd="@dimen/tooltip_horizontal_padding" - android:paddingTop="@dimen/tooltip_vertical_padding" - android:paddingBottom="@dimen/tooltip_vertical_padding" + android:paddingStart="?attr/tooltipHorizontalPadding" + android:paddingEnd="?attr/tooltipHorizontalPadding" + android:paddingTop="?attr/tooltipVerticalPadding" + android:paddingBottom="?attr/tooltipVerticalPadding" android:maxWidth="256dp" android:background="?android:attr/tooltipFrameBackground" android:textAppearance="@style/TextAppearance.Tooltip" diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 7cc9e13db5cf..440219de9561 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -1078,6 +1078,11 @@ <!-- Background color to use for tooltip popups. --> <attr name="tooltipBackgroundColor" format="reference|color" /> + <attr name="tooltipCornerRadius" format="dimension" /> + <attr name="tooltipHorizontalPadding" format="dimension" /> + <attr name="tooltipVerticalPadding" format="dimension" /> + <attr name="tooltipFontSize" format="dimension" /> + <!-- Theme to use for Search Dialogs. --> <attr name="searchDialogTheme" format="reference" /> diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index 2e3dbda5e41c..0be33c2e7a03 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -3296,8 +3296,8 @@ usually TVs. <p>Requires permission {@code android.permission.DISABLE_SYSTEM_SOUND_EFFECTS}. --> <attr name="playHomeTransitionSound" format="boolean"/> - <!-- Indicates whether the activity can be displayed on a remote device which may or - may not be running Android. --> + <!-- Indicates whether the activity can be displayed on a display that may belong to a + remote device which may or may not be running Android. --> <attr name="canDisplayOnRemoteDevices" format="boolean"/> <attr name="allowUntrustedActivityEmbedding" /> <attr name="knownActivityEmbeddingCerts" /> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 2afc30315bd1..8ed444d39f2f 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -1399,6 +1399,10 @@ Settings.System.RING_VIBRATION_INTENSITY more details on the constant values and meanings. --> <integer name="config_defaultRingVibrationIntensity">2</integer> + <!-- The default intensity level for keyboard vibrations. Note that this will only be applied + on devices where config_keyboardVibrationSettingsSupported is true, otherwise the + keyboard vibration will follow config_defaultHapticFeedbackIntensity --> + <integer name="config_defaultKeyboardVibrationIntensity">2</integer> <!-- Whether to use the strict phone number matcher by default. --> <bool name="config_use_strict_phone_number_comparation">false</bool> diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 6cba84be58c3..77b5587e77be 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -771,6 +771,7 @@ <dimen name="tooltip_precise_anchor_threshold">96dp</dimen> <!-- Extra tooltip offset used when anchoring to the mouse/touch position --> <dimen name="tooltip_precise_anchor_extra_offset">8dp</dimen> + <dimen name="tooltip_font_size">14sp</dimen> <!-- The max amount of scroll ItemTouchHelper will trigger if dragged view is out of RecyclerView's bounds.--> diff --git a/core/res/res/values/dimens_material.xml b/core/res/res/values/dimens_material.xml index 972fe7ed91de..35f35fb86a59 100644 --- a/core/res/res/values/dimens_material.xml +++ b/core/res/res/values/dimens_material.xml @@ -204,4 +204,9 @@ <dimen name="progress_bar_size_small">16dip</dimen> <dimen name="progress_bar_size_medium">48dp</dimen> <dimen name="progress_bar_size_large">76dp</dimen> + + <dimen name="tooltip_corner_radius_material">4dp</dimen> + <dimen name="tooltip_horizontal_padding_material">8dp</dimen> + <dimen name="tooltip_vertical_padding_material">4dp</dimen> + <dimen name="tooltip_font_size_material">12sp</dimen> </resources> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index ec865f6c376f..e94db2dc7fc4 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -6555,4 +6555,7 @@ ul.</string> <string name="keyboard_shortcut_group_applications_maps">Maps</string> <!-- User visible title for the keyboard shortcut group containing system-wide application launch shortcuts. [CHAR-LIMIT=70] --> <string name="keyboard_shortcut_group_applications">Applications</string> + + <!-- Fingerprint loe notification string --> + <string name="fingerprint_loe_notification_msg">Your fingerprints can no longer be recognized. Set up Fingerprint Unlock again.</string> </resources> diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml index aabc8ca5aef6..c084b4c1e834 100644 --- a/core/res/res/values/styles.xml +++ b/core/res/res/values/styles.xml @@ -998,7 +998,7 @@ please see styles_device_defaults.xml. <style name="TextAppearance.Tooltip"> <item name="fontFamily">sans-serif</item> - <item name="textSize">14sp</item> + <item name="textSize">?android:attr/tooltipFontSize</item> </style> <style name="Widget.ActivityChooserView"> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index bc8c778c0671..cbf3fe7b0a1b 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4223,6 +4223,7 @@ <java-symbol type="integer" name="config_defaultMediaVibrationIntensity" /> <java-symbol type="integer" name="config_defaultNotificationVibrationIntensity" /> <java-symbol type="integer" name="config_defaultRingVibrationIntensity" /> + <java-symbol type="integer" name="config_defaultKeyboardVibrationIntensity" /> <java-symbol type="bool" name="config_maskMainBuiltInDisplayCutout" /> @@ -5583,4 +5584,7 @@ <java-symbol type="string" name="keyboard_shortcut_group_applications_music" /> <java-symbol type="string" name="keyboard_shortcut_group_applications_sms" /> <java-symbol type="string" name="keyboard_shortcut_group_applications" /> + + <!-- Fingerprint loe notification string --> + <java-symbol type="string" name="fingerprint_loe_notification_msg" /> </resources> diff --git a/core/res/res/values/themes.xml b/core/res/res/values/themes.xml index c3d304dc35e1..3b3bb8dfc405 100644 --- a/core/res/res/values/themes.xml +++ b/core/res/res/values/themes.xml @@ -461,6 +461,10 @@ please see themes_device_defaults.xml. <item name="tooltipFrameBackground">@drawable/tooltip_frame</item> <item name="tooltipForegroundColor">@color/bright_foreground_light</item> <item name="tooltipBackgroundColor">@color/tooltip_background_light</item> + <item name="tooltipCornerRadius">@dimen/tooltip_corner_radius</item> + <item name="tooltipHorizontalPadding">@dimen/tooltip_horizontal_padding</item> + <item name="tooltipVerticalPadding">@dimen/tooltip_vertical_padding</item> + <item name="tooltipFontSize">@dimen/tooltip_font_size</item> <!-- Autofill: max width/height of the dataset picker as a fraction of screen size --> <item name="autofillDatasetPickerMaxWidth">@dimen/autofill_dataset_picker_max_width</item> @@ -582,9 +586,10 @@ please see themes_device_defaults.xml. <item name="floatingToolbarOpenDrawable">@drawable/ic_menu_moreoverflow_material_light</item> <item name="floatingToolbarDividerColor">@color/floating_popup_divider_light</item> - <!-- Tooltip popup colors --> + <!-- Tooltip popup styles --> <item name="tooltipForegroundColor">@color/bright_foreground_dark</item> <item name="tooltipBackgroundColor">@color/tooltip_background_dark</item> + </style> <!-- Variant of {@link #Theme_Light} with no title bar --> diff --git a/core/res/res/values/themes_material.xml b/core/res/res/values/themes_material.xml index 8e2fb34ec8a4..9f11208c97ec 100644 --- a/core/res/res/values/themes_material.xml +++ b/core/res/res/values/themes_material.xml @@ -408,8 +408,12 @@ please see themes_device_defaults.xml. <item name="colorProgressBackgroundNormal">?attr/colorControlNormal</item> <!-- Tooltip popup properties --> - <item name="tooltipForegroundColor">@color/foreground_material_light</item> - <item name="tooltipBackgroundColor">@color/tooltip_background_light</item> + <item name="tooltipForegroundColor">@color/system_on_surface_light</item> + <item name="tooltipBackgroundColor">@color/system_surface_light</item> + <item name="tooltipCornerRadius">@dimen/tooltip_corner_radius_material</item> + <item name="tooltipHorizontalPadding">@dimen/tooltip_horizontal_padding_material</item> + <item name="tooltipVerticalPadding">@dimen/tooltip_vertical_padding_material</item> + <item name="tooltipFontSize">@dimen/tooltip_font_size_material</item> </style> <!-- Material theme (light version). --> @@ -785,8 +789,13 @@ please see themes_device_defaults.xml. <item name="colorProgressBackgroundNormal">?attr/colorControlNormal</item> <!-- Tooltip popup properties --> - <item name="tooltipForegroundColor">@color/foreground_material_dark</item> - <item name="tooltipBackgroundColor">@color/tooltip_background_dark</item> + <item name="tooltipForegroundColor">@color/system_on_surface_dark</item> + <item name="tooltipBackgroundColor">@color/system_surface_dark</item> + <item name="tooltipCornerRadius">@dimen/tooltip_corner_radius_material</item> + <item name="tooltipHorizontalPadding">@dimen/tooltip_horizontal_padding_material</item> + <item name="tooltipVerticalPadding">@dimen/tooltip_vertical_padding_material</item> + <item name="tooltipFontSize">@dimen/tooltip_font_size_material</item> + </style> <!-- Variant of the material (light) theme that has a solid (opaque) action bar diff --git a/core/tests/coretests/src/android/animation/ValueAnimatorTests.java b/core/tests/coretests/src/android/animation/ValueAnimatorTests.java index a102b3ed9971..eb463fd9a76b 100644 --- a/core/tests/coretests/src/android/animation/ValueAnimatorTests.java +++ b/core/tests/coretests/src/android/animation/ValueAnimatorTests.java @@ -30,9 +30,9 @@ import android.os.SystemClock; import android.view.Choreographer; import android.view.animation.LinearInterpolator; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.MediumTest; import androidx.test.rule.ActivityTestRule; -import androidx.test.runner.AndroidJUnit4; import org.junit.After; import org.junit.Before; diff --git a/core/tests/coretests/src/android/companion/virtual/audio/VirtualAudioSessionTest.java b/core/tests/coretests/src/android/companion/virtual/audio/VirtualAudioSessionTest.java index e025fae4b909..b91263ea6b3c 100644 --- a/core/tests/coretests/src/android/companion/virtual/audio/VirtualAudioSessionTest.java +++ b/core/tests/coretests/src/android/companion/virtual/audio/VirtualAudioSessionTest.java @@ -35,7 +35,7 @@ import android.media.AudioRecordingConfiguration; import android.platform.test.annotations.Presubmit; import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Before; import org.junit.Rule; diff --git a/core/tests/coretests/src/android/debug/AdbNotificationsTest.java b/core/tests/coretests/src/android/debug/AdbNotificationsTest.java index 3496e2c7fea3..10eeb35855b9 100644 --- a/core/tests/coretests/src/android/debug/AdbNotificationsTest.java +++ b/core/tests/coretests/src/android/debug/AdbNotificationsTest.java @@ -25,8 +25,8 @@ import android.platform.test.annotations.Presubmit; import android.text.TextUtils; import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Before; import org.junit.Test; diff --git a/core/tests/coretests/src/android/graphics/FontListParserTest.java b/core/tests/coretests/src/android/graphics/FontListParserTest.java index 5f96c1789015..52f53ddb4356 100644 --- a/core/tests/coretests/src/android/graphics/FontListParserTest.java +++ b/core/tests/coretests/src/android/graphics/FontListParserTest.java @@ -16,16 +16,16 @@ package android.graphics; +import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE; +import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL; +import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY; +import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT; import static android.graphics.fonts.FontStyle.FONT_SLANT_ITALIC; import static android.graphics.fonts.FontStyle.FONT_SLANT_UPRIGHT; import static android.graphics.fonts.FontStyle.FONT_WEIGHT_NORMAL; import static android.text.FontConfig.FontFamily.VARIANT_COMPACT; import static android.text.FontConfig.FontFamily.VARIANT_DEFAULT; import static android.text.FontConfig.FontFamily.VARIANT_ELEGANT; -import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE; -import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY; -import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL; -import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT; import static com.google.common.truth.Truth.assertThat; @@ -38,8 +38,8 @@ import android.os.LocaleList; import android.text.FontConfig; import android.util.Xml; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/coretests/src/android/graphics/RectTest.java b/core/tests/coretests/src/android/graphics/RectTest.java index 2918f44ad65d..d0cb5d5ea416 100644 --- a/core/tests/coretests/src/android/graphics/RectTest.java +++ b/core/tests/coretests/src/android/graphics/RectTest.java @@ -24,8 +24,8 @@ import static org.junit.Assert.assertNull; import android.platform.test.annotations.Presubmit; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/coretests/src/android/graphics/TypefaceEqualsTest.java b/core/tests/coretests/src/android/graphics/TypefaceEqualsTest.java index 6ae7eb72fab2..a94f41279392 100644 --- a/core/tests/coretests/src/android/graphics/TypefaceEqualsTest.java +++ b/core/tests/coretests/src/android/graphics/TypefaceEqualsTest.java @@ -23,8 +23,8 @@ import android.content.res.AssetManager; import android.graphics.fonts.Font; import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java b/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java index 0d687b24a4e5..10aed8d51d09 100644 --- a/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java +++ b/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java @@ -39,8 +39,8 @@ import android.text.FontConfig; import android.util.ArrayMap; import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.After; import org.junit.Before; diff --git a/core/tests/coretests/src/android/graphics/TypefaceTest.java b/core/tests/coretests/src/android/graphics/TypefaceTest.java index 6bf8f5678b33..80efa511d163 100644 --- a/core/tests/coretests/src/android/graphics/TypefaceTest.java +++ b/core/tests/coretests/src/android/graphics/TypefaceTest.java @@ -30,10 +30,10 @@ import android.text.FontConfig; import android.util.ArrayMap; import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; import androidx.test.filters.MediumTest; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.frameworks.coretests.R; diff --git a/core/tests/coretests/src/android/graphics/drawable/DrawableWrapperTest.java b/core/tests/coretests/src/android/graphics/drawable/DrawableWrapperTest.java index d0a6ff9251cc..4991cd0a1347 100644 --- a/core/tests/coretests/src/android/graphics/drawable/DrawableWrapperTest.java +++ b/core/tests/coretests/src/android/graphics/drawable/DrawableWrapperTest.java @@ -25,8 +25,8 @@ import android.graphics.PorterDuff.Mode; import android.graphics.PorterDuffXfermode; import android.graphics.Xfermode; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/coretests/src/android/hardware/input/InputFlagsTest.java b/core/tests/coretests/src/android/hardware/input/InputFlagsTest.java index 5aeab42eaaea..b4f1deebd796 100644 --- a/core/tests/coretests/src/android/hardware/input/InputFlagsTest.java +++ b/core/tests/coretests/src/android/hardware/input/InputFlagsTest.java @@ -21,8 +21,8 @@ import static com.android.hardware.input.Flags.keyboardLayoutPreviewFlag; import android.platform.test.annotations.Presubmit; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/coretests/src/android/net/NetworkKeyTest.java b/core/tests/coretests/src/android/net/NetworkKeyTest.java index b13bcd1311f6..444ed51fa823 100644 --- a/core/tests/coretests/src/android/net/NetworkKeyTest.java +++ b/core/tests/coretests/src/android/net/NetworkKeyTest.java @@ -25,7 +25,7 @@ import android.net.wifi.ScanResult; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Before; import org.junit.Test; diff --git a/core/tests/coretests/src/android/net/NetworkRecommendationProviderTest.java b/core/tests/coretests/src/android/net/NetworkRecommendationProviderTest.java index 3e45a79951d3..46f22cec4213 100644 --- a/core/tests/coretests/src/android/net/NetworkRecommendationProviderTest.java +++ b/core/tests/coretests/src/android/net/NetworkRecommendationProviderTest.java @@ -26,7 +26,7 @@ import static org.mockito.Matchers.eq; import android.Manifest.permission; import android.content.Context; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Before; import org.junit.Test; diff --git a/core/tests/coretests/src/android/net/SSLCertificateSocketFactoryTest.java b/core/tests/coretests/src/android/net/SSLCertificateSocketFactoryTest.java index bc12e727c5f0..7413ede92914 100644 --- a/core/tests/coretests/src/android/net/SSLCertificateSocketFactoryTest.java +++ b/core/tests/coretests/src/android/net/SSLCertificateSocketFactoryTest.java @@ -19,7 +19,7 @@ package android.net; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/coretests/src/android/net/ScoredNetworkTest.java b/core/tests/coretests/src/android/net/ScoredNetworkTest.java index d984d86e1147..63eeaa1e97e0 100644 --- a/core/tests/coretests/src/android/net/ScoredNetworkTest.java +++ b/core/tests/coretests/src/android/net/ScoredNetworkTest.java @@ -26,7 +26,7 @@ import static org.junit.Assert.fail; import android.os.Bundle; import android.os.Parcel; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/coretests/src/android/net/SntpClientTest.java b/core/tests/coretests/src/android/net/SntpClientTest.java index 267fc2b636d6..024d614814a1 100644 --- a/core/tests/coretests/src/android/net/SntpClientTest.java +++ b/core/tests/coretests/src/android/net/SntpClientTest.java @@ -29,7 +29,7 @@ import android.net.sntp.Timestamp64; import android.platform.test.annotations.Presubmit; import android.util.Log; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.ext.junit.runners.AndroidJUnit4; import libcore.util.HexEncoding; diff --git a/core/tests/coretests/src/android/net/sntp/Duration64Test.java b/core/tests/coretests/src/android/net/sntp/Duration64Test.java index b2285962f82d..b177e18a5d8a 100644 --- a/core/tests/coretests/src/android/net/sntp/Duration64Test.java +++ b/core/tests/coretests/src/android/net/sntp/Duration64Test.java @@ -23,7 +23,7 @@ import static org.junit.Assert.assertTrue; import android.platform.test.annotations.Presubmit; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/coretests/src/android/net/sntp/Timestamp64Test.java b/core/tests/coretests/src/android/net/sntp/Timestamp64Test.java index 200c80e81588..9f95132a8437 100644 --- a/core/tests/coretests/src/android/net/sntp/Timestamp64Test.java +++ b/core/tests/coretests/src/android/net/sntp/Timestamp64Test.java @@ -23,7 +23,7 @@ import static org.junit.Assert.fail; import android.platform.test.annotations.Presubmit; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/coretests/src/android/preference/PreferenceIconSpaceTest.java b/core/tests/coretests/src/android/preference/PreferenceIconSpaceTest.java index 0deb77e60a51..55a347ec2227 100644 --- a/core/tests/coretests/src/android/preference/PreferenceIconSpaceTest.java +++ b/core/tests/coretests/src/android/preference/PreferenceIconSpaceTest.java @@ -27,8 +27,8 @@ import android.view.ViewGroup; import android.widget.ImageView; import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Before; import org.junit.Test; diff --git a/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java b/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java index c25aa51c6b1e..746c8cafe1e7 100644 --- a/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java +++ b/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java @@ -42,9 +42,9 @@ import android.print.test.services.PrinterDiscoverySessionCallbacks; import android.print.test.services.StubbablePrinterDiscoverySession; import android.printservice.recommendation.IRecommendationsChangeListener; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; import androidx.test.filters.MediumTest; -import androidx.test.runner.AndroidJUnit4; import androidx.test.uiautomator.UiDevice; import org.junit.Before; diff --git a/core/tests/coretests/src/android/provider/DeviceConfigServiceManagerTest.java b/core/tests/coretests/src/android/provider/DeviceConfigServiceManagerTest.java index e20258a625dd..a60746f4047c 100644 --- a/core/tests/coretests/src/android/provider/DeviceConfigServiceManagerTest.java +++ b/core/tests/coretests/src/android/provider/DeviceConfigServiceManagerTest.java @@ -23,8 +23,8 @@ import static org.junit.Assume.assumeTrue; import android.platform.test.annotations.Presubmit; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Before; import org.junit.Test; diff --git a/core/tests/coretests/src/android/provider/DeviceConfigTest.java b/core/tests/coretests/src/android/provider/DeviceConfigTest.java index 9300d1e5cb95..681396e6011a 100644 --- a/core/tests/coretests/src/android/provider/DeviceConfigTest.java +++ b/core/tests/coretests/src/android/provider/DeviceConfigTest.java @@ -29,9 +29,9 @@ import android.os.Bundle; import android.platform.test.annotations.Presubmit; import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.FlakyTest; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.After; import org.junit.Assert; diff --git a/core/tests/coretests/src/android/provider/FontsContractE2ETest.java b/core/tests/coretests/src/android/provider/FontsContractE2ETest.java index 7e02be85f01a..401017129fa3 100644 --- a/core/tests/coretests/src/android/provider/FontsContractE2ETest.java +++ b/core/tests/coretests/src/android/provider/FontsContractE2ETest.java @@ -33,8 +33,8 @@ import android.os.Handler; import android.provider.FontsContract.FontFamilyResult; import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.After; import org.junit.Before; diff --git a/core/tests/coretests/src/android/service/controls/ControlProviderServiceTest.java b/core/tests/coretests/src/android/service/controls/ControlProviderServiceTest.java index 4d4469011c06..6eaf2e4890a3 100644 --- a/core/tests/coretests/src/android/service/controls/ControlProviderServiceTest.java +++ b/core/tests/coretests/src/android/service/controls/ControlProviderServiceTest.java @@ -45,9 +45,9 @@ import android.service.controls.actions.ControlAction; import android.service.controls.actions.ControlActionWrapper; import android.service.controls.templates.ThumbnailTemplate; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; import com.android.internal.R; diff --git a/core/tests/coretests/src/android/service/controls/actions/ControlActionTest.java b/core/tests/coretests/src/android/service/controls/actions/ControlActionTest.java index d8088b7735ad..44bdc53af1d4 100644 --- a/core/tests/coretests/src/android/service/controls/actions/ControlActionTest.java +++ b/core/tests/coretests/src/android/service/controls/actions/ControlActionTest.java @@ -23,8 +23,8 @@ import static org.junit.Assert.assertNotNull; import android.os.Parcel; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/coretests/src/android/service/controls/templates/ControlTemplateTest.java b/core/tests/coretests/src/android/service/controls/templates/ControlTemplateTest.java index 91a3ba7d0e74..73b6f6485db1 100644 --- a/core/tests/coretests/src/android/service/controls/templates/ControlTemplateTest.java +++ b/core/tests/coretests/src/android/service/controls/templates/ControlTemplateTest.java @@ -25,8 +25,8 @@ import android.annotation.DrawableRes; import android.graphics.drawable.Icon; import android.os.Parcel; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.frameworks.coretests.R; diff --git a/core/tests/coretests/src/android/service/euicc/EuiccProfileInfoTest.java b/core/tests/coretests/src/android/service/euicc/EuiccProfileInfoTest.java index 6792d0b91084..f4206c85b6e6 100644 --- a/core/tests/coretests/src/android/service/euicc/EuiccProfileInfoTest.java +++ b/core/tests/coretests/src/android/service/euicc/EuiccProfileInfoTest.java @@ -26,8 +26,8 @@ import android.os.Parcel; import android.service.carrier.CarrierIdentifier; import android.telephony.UiccAccessRule; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/coretests/src/android/service/notification/NotificationListenerFilterTest.java b/core/tests/coretests/src/android/service/notification/NotificationListenerFilterTest.java index a121941e7b73..44456e94dd59 100644 --- a/core/tests/coretests/src/android/service/notification/NotificationListenerFilterTest.java +++ b/core/tests/coretests/src/android/service/notification/NotificationListenerFilterTest.java @@ -27,8 +27,8 @@ import android.content.pm.VersionedPackage; import android.os.Parcel; import android.util.ArraySet; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/coretests/src/android/service/notification/StatusBarNotificationTest.java b/core/tests/coretests/src/android/service/notification/StatusBarNotificationTest.java index 76c9f8892105..504240812559 100644 --- a/core/tests/coretests/src/android/service/notification/StatusBarNotificationTest.java +++ b/core/tests/coretests/src/android/service/notification/StatusBarNotificationTest.java @@ -37,8 +37,8 @@ import android.metrics.LogMaker; import android.os.UserHandle; import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; diff --git a/core/tests/coretests/src/android/service/quicksettings/TileTest.java b/core/tests/coretests/src/android/service/quicksettings/TileTest.java index ca6c3b443aa6..43f9122bf3da 100644 --- a/core/tests/coretests/src/android/service/quicksettings/TileTest.java +++ b/core/tests/coretests/src/android/service/quicksettings/TileTest.java @@ -18,8 +18,8 @@ package android.service.quicksettings; import static com.google.common.truth.Truth.assertThat; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/coretests/src/android/service/settings/suggestions/SuggestionServiceTest.java b/core/tests/coretests/src/android/service/settings/suggestions/SuggestionServiceTest.java index 64edda5ee879..85659d68c5a5 100644 --- a/core/tests/coretests/src/android/service/settings/suggestions/SuggestionServiceTest.java +++ b/core/tests/coretests/src/android/service/settings/suggestions/SuggestionServiceTest.java @@ -23,9 +23,9 @@ import android.os.IBinder; import android.os.RemoteException; import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import androidx.test.rule.ServiceTestRule; -import androidx.test.runner.AndroidJUnit4; import org.junit.After; import org.junit.Before; diff --git a/core/tests/coretests/src/android/service/settings/suggestions/SuggestionTest.java b/core/tests/coretests/src/android/service/settings/suggestions/SuggestionTest.java index e0eb197f437a..03096de4b0d5 100644 --- a/core/tests/coretests/src/android/service/settings/suggestions/SuggestionTest.java +++ b/core/tests/coretests/src/android/service/settings/suggestions/SuggestionTest.java @@ -26,8 +26,8 @@ import android.graphics.drawable.Icon; import android.os.Parcel; import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Before; import org.junit.Test; diff --git a/core/tests/coretests/src/android/telephony/PinResultTest.java b/core/tests/coretests/src/android/telephony/PinResultTest.java index c260807e5cbc..f5432ee2da18 100644 --- a/core/tests/coretests/src/android/telephony/PinResultTest.java +++ b/core/tests/coretests/src/android/telephony/PinResultTest.java @@ -18,7 +18,7 @@ package android.telephony; import static com.google.common.truth.Truth.assertThat; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java b/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java index df9a89e07404..bbeb18dfbecd 100644 --- a/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java +++ b/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java @@ -37,7 +37,7 @@ import android.tools.traces.monitors.TraceMonitor; import android.util.proto.ProtoInputStream; import android.util.proto.ProtoOutputStream; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.truth.Truth; import com.google.protobuf.InvalidProtocolBufferException; diff --git a/core/tests/coretests/src/android/transition/AutoTransitionTest.java b/core/tests/coretests/src/android/transition/AutoTransitionTest.java index deae967a3e72..5d58feadc25b 100644 --- a/core/tests/coretests/src/android/transition/AutoTransitionTest.java +++ b/core/tests/coretests/src/android/transition/AutoTransitionTest.java @@ -20,8 +20,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/coretests/src/com/android/internal/infra/AndroidFutureTest.java b/core/tests/coretests/src/com/android/internal/infra/AndroidFutureTest.java index 3a272256e60e..178e93a6a37b 100644 --- a/core/tests/coretests/src/com/android/internal/infra/AndroidFutureTest.java +++ b/core/tests/coretests/src/com/android/internal/infra/AndroidFutureTest.java @@ -22,7 +22,7 @@ import static org.testng.Assert.expectThrows; import android.os.Parcel; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/coretests/src/com/android/internal/infra/ServiceConnectorTest.java b/core/tests/coretests/src/com/android/internal/infra/ServiceConnectorTest.java index 725dcf30d485..3d1b565cdc54 100644 --- a/core/tests/coretests/src/com/android/internal/infra/ServiceConnectorTest.java +++ b/core/tests/coretests/src/com/android/internal/infra/ServiceConnectorTest.java @@ -29,8 +29,8 @@ import android.os.Process; import android.os.UserHandle; import androidx.annotation.NonNull; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; import com.android.frameworks.coretests.aidl.ITestServiceConnectorService; import com.android.internal.infra.ServiceConnectorTest.CapturingServiceLifecycleCallbacks.ServiceLifeCycleEvent; diff --git a/core/tests/coretests/src/com/android/internal/logging/MetricsLoggerTest.java b/core/tests/coretests/src/com/android/internal/logging/MetricsLoggerTest.java index 7054cc0f24b4..b86cb4ad2339 100644 --- a/core/tests/coretests/src/com/android/internal/logging/MetricsLoggerTest.java +++ b/core/tests/coretests/src/com/android/internal/logging/MetricsLoggerTest.java @@ -20,8 +20,8 @@ import static com.google.common.truth.Truth.assertThat; import android.metrics.LogMaker; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.internal.logging.nano.MetricsProto; import com.android.internal.logging.testing.FakeMetricsLogger; diff --git a/core/tests/coretests/src/com/android/internal/logging/UiEventLoggerTest.java b/core/tests/coretests/src/com/android/internal/logging/UiEventLoggerTest.java index 7840f7177278..fc2862756d8b 100644 --- a/core/tests/coretests/src/com/android/internal/logging/UiEventLoggerTest.java +++ b/core/tests/coretests/src/com/android/internal/logging/UiEventLoggerTest.java @@ -18,8 +18,8 @@ package com.android.internal.logging; import static com.google.common.truth.Truth.assertThat; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.internal.logging.testing.UiEventLoggerFake; diff --git a/core/tests/coretests/src/com/android/internal/ravenwood/RavenwoodEnvironmentTest.java b/core/tests/coretests/src/com/android/internal/ravenwood/RavenwoodEnvironmentTest.java index d1ef61b2e365..d1c066821cff 100644 --- a/core/tests/coretests/src/com/android/internal/ravenwood/RavenwoodEnvironmentTest.java +++ b/core/tests/coretests/src/com/android/internal/ravenwood/RavenwoodEnvironmentTest.java @@ -19,7 +19,7 @@ import static junit.framework.TestCase.assertEquals; import android.platform.test.ravenwood.RavenwoodRule; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Rule; import org.junit.Test; diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 8e1fde066277..409cde30cf8c 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -119,7 +119,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // TODO(b/243518738): Move to WM Extensions if we have requirement of overlay without // association. It's not set in WM Extensions nor Wm Jetpack library currently. - private static final String KEY_OVERLAY_ASSOCIATE_WITH_LAUNCHING_ACTIVITY = + @VisibleForTesting + static final String KEY_OVERLAY_ASSOCIATE_WITH_LAUNCHING_ACTIVITY = "androidx.window.extensions.embedding.shouldAssociateWithLaunchingActivity"; @VisibleForTesting @@ -2742,89 +2743,70 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } final int taskId = getTaskId(launchActivity); - if (!overlayContainers.isEmpty()) { - for (final TaskFragmentContainer overlayContainer : overlayContainers) { - final boolean isTopNonFinishingOverlay = overlayContainer.equals( - overlayContainer.getTaskContainer().getTopNonFinishingTaskFragmentContainer( - true /* includePin */, true /* includeOverlay */)); - if (taskId != overlayContainer.getTaskId()) { - // If there's an overlay container with same tag in a different task, - // dismiss the overlay container since the tag must be unique per process. - if (overlayTag.equals(overlayContainer.getOverlayTag())) { - Log.w(TAG, "The overlay container with tag:" - + overlayContainer.getOverlayTag() + " is dismissed because" - + " there's an existing overlay container with the same tag but" - + " different task ID:" + overlayContainer.getTaskId() + ". " - + "The new associated activity is " + launchActivity); - mPresenter.cleanupContainer(wct, overlayContainer, - false /* shouldFinishDependant */); - } - continue; - } - if (!overlayTag.equals(overlayContainer.getOverlayTag())) { - // If there's an overlay container with different tag on top in the same - // task, dismiss the existing overlay container. - if (isTopNonFinishingOverlay) { - mPresenter.cleanupContainer(wct, overlayContainer, - false /* shouldFinishDependant */); - } - continue; - } - // The overlay container has the same tag and task ID with the new launching - // overlay container. - if (!isTopNonFinishingOverlay) { - // Dismiss the invisible overlay container regardless of activity - // association if it collides the tag of new launched overlay container . - Log.w(TAG, "The invisible overlay container with tag:" - + overlayContainer.getOverlayTag() + " is dismissed because" - + " there's a launching overlay container with the same tag." - + " The new associated activity is " + launchActivity); - mPresenter.cleanupContainer(wct, overlayContainer, - false /* shouldFinishDependant */); - continue; - } - // Requesting an always-on-top overlay. - if (!associateLaunchingActivity) { - if (overlayContainer.isOverlayWithActivityAssociation()) { - // Dismiss the overlay container since it has associated with an activity. - Log.w(TAG, "The overlay container with tag:" - + overlayContainer.getOverlayTag() + " is dismissed because" - + " there's an existing overlay container with the same tag but" - + " different associated launching activity. The overlay container" - + " doesn't associate with any activity."); - mPresenter.cleanupContainer(wct, overlayContainer, - false /* shouldFinishDependant */); - continue; - } else { - // The existing overlay container doesn't associate an activity as well. - // Just update the overlay and return. - // Note that going to this condition means the tag, task ID matches a - // visible always-on-top overlay, and won't dismiss any overlay any more. - mPresenter.applyActivityStackAttributes(wct, overlayContainer, attrs, - getMinDimensions(intent)); - return overlayContainer; - } - } - if (launchActivity.getActivityToken() - != overlayContainer.getAssociatedActivityToken()) { - Log.w(TAG, "The overlay container with tag:" - + overlayContainer.getOverlayTag() + " is dismissed because" - + " there's an existing overlay container with the same tag but" - + " different associated launching activity. The new associated" - + " activity is " + launchActivity); - // The associated activity must be the same, or it will be dismissed. - mPresenter.cleanupContainer(wct, overlayContainer, - false /* shouldFinishDependant */); - continue; - } - // Reaching here means the launching activity launch an overlay container with the - // same task ID, tag, while there's a previously launching visible overlay - // container. We'll regard it as updating the existing overlay container. + // Overlay container policy: + // 1. Overlay tag must be unique per process. + // a. For associated overlay, if a new launched overlay container has the same tag as + // an existing one, the existing overlay will be dismissed regardless of its task + // and window hierarchy. + // b. For always-on-top overlay, if there's an overlay container has the same tag in the + // launched task, the overlay container will be re-used, which means the + // ActivityStackAttributes will be applied and the launched activity will be positioned + // on top of the overlay container. + // 2. There must be at most one overlay that partially occludes a visible activity per task. + // a. For associated overlay, only the top visible overlay container in the launched task + // will be dismissed. + // b. Always-on-top overlay is always visible. If there's an overlay with different tags + // in the same task, the overlay will be dismissed in case an activity above + // the overlay is dismissed and the overlay is shown unexpectedly. + for (final TaskFragmentContainer overlayContainer : overlayContainers) { + final boolean isTopNonFinishingOverlay = overlayContainer.isTopNonFinishingChild(); + final boolean areInSameTask = taskId == overlayContainer.getTaskId(); + final boolean haveSameTag = overlayTag.equals(overlayContainer.getOverlayTag()); + if (!associateLaunchingActivity && overlayContainer.isAlwaysOnTopOverlay() + && haveSameTag && areInSameTask) { + // Just launch the activity and update the existing always-on-top overlay + // if the requested overlay is an always-on-top overlay with the same tag + // as the existing one. mPresenter.applyActivityStackAttributes(wct, overlayContainer, attrs, getMinDimensions(intent)); return overlayContainer; - } + if (haveSameTag) { + // For other tag match, we should clean up the existing overlay since the overlay + // tag must be unique per process. + Log.w(TAG, "The overlay container with tag:" + + overlayContainer.getOverlayTag() + " is dismissed with " + + " the launching activity=" + launchActivity + + " because there's an existing overlay container with the same tag."); + mPresenter.cleanupContainer(wct, overlayContainer, + false /* shouldFinishDependant */); + } + if (!areInSameTask) { + // Early return here because we won't clean-up or update overlay from different + // tasks except tag collision. + continue; + } + if (associateLaunchingActivity) { + // For associated overlay, we only dismiss the overlay if it's the top non-finishing + // child of its parent container. + if (isTopNonFinishingOverlay) { + Log.w(TAG, "The on-top overlay container with tag:" + + overlayContainer.getOverlayTag() + " is dismissed with " + + " the launching activity=" + launchActivity + + "because we only allow one overlay on top."); + mPresenter.cleanupContainer(wct, overlayContainer, + false /* shouldFinishDependant */); + } + continue; + } + // Otherwise, we should clean up the overlay in the task because we only allow one + // overlay when an always-on-top overlay is launched. + Log.w(TAG, "The overlay container with tag:" + + overlayContainer.getOverlayTag() + " is dismissed with " + + " the launching activity=" + launchActivity + + "because an always-on-top overlay is launched."); + mPresenter.cleanupContainer(wct, overlayContainer, + false /* shouldFinishDependant */); } // Launch the overlay container to the task with taskId. return createEmptyContainer(wct, intent, taskId, attrs, launchActivity, overlayTag, diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java index 7173b0c95230..d0e2c998e961 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -340,6 +340,13 @@ class TaskFragmentContainer { return mInfo != null && mInfo.isVisible(); } + /** + * See {@link TaskFragmentInfo#isTopNonFinishingChild()} + */ + boolean isTopNonFinishingChild() { + return mInfo != null && mInfo.isTopNonFinishingChild(); + } + /** Whether the TaskFragment is in an intermediate state waiting for the server update.*/ boolean isInIntermediateState() { if (mInfo == null) { diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java index d649c6d57137..7dc78fdd601f 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java @@ -163,12 +163,14 @@ public class EmbeddingTestUtils { } /** Creates a mock TaskFragmentInfo for the given TaskFragment. */ + @NonNull static TaskFragmentInfo createMockTaskFragmentInfo(@NonNull TaskFragmentContainer container, @NonNull Activity activity) { return createMockTaskFragmentInfo(container, activity, true /* isVisible */); } /** Creates a mock TaskFragmentInfo for the given TaskFragment. */ + @NonNull static TaskFragmentInfo createMockTaskFragmentInfo(@NonNull TaskFragmentContainer container, @NonNull Activity activity, boolean isVisible) { return new TaskFragmentInfo(container.getTaskFragmentToken(), @@ -182,7 +184,27 @@ public class EmbeddingTestUtils { false /* isTaskClearedForReuse */, false /* isTaskFragmentClearedForPip */, false /* isClearedForReorderActivityToFront */, - new Point()); + new Point(), + false /* isTopChild */); + } + + /** Creates a mock TaskFragmentInfo for the given TaskFragment. */ + @NonNull + static TaskFragmentInfo createMockTaskFragmentInfo(@NonNull TaskFragmentContainer container, + @NonNull Activity activity, boolean isVisible, boolean isOnTop) { + return new TaskFragmentInfo(container.getTaskFragmentToken(), + mock(WindowContainerToken.class), + new Configuration(), + 1, + isVisible, + Collections.singletonList(activity.getActivityToken()), + new ArrayList<>(), + new Point(), + false /* isTaskClearedForReuse */, + false /* isTaskFragmentClearedForPip */, + false /* isClearedForReorderActivityToFront */, + new Point(), + isOnTop); } static ActivityInfo createActivityInfoWithMinDimensions() { diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java index ad41b18dcbc6..8911d18b9b97 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java @@ -114,6 +114,7 @@ public class JetpackTaskFragmentOrganizerTest { mock(WindowContainerToken.class), new Configuration(), 0 /* runningActivityCount */, false /* isVisible */, new ArrayList<>(), new ArrayList<>(), new Point(), false /* isTaskClearedForReuse */, false /* isTaskFragmentClearedForPip */, - false /* isClearedForReorderActivityToFront */, new Point()); + false /* isClearedForReorderActivityToFront */, new Point(), + false /* isTopChild */); } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java index 1c4c8870b26f..475475b05272 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java @@ -30,6 +30,7 @@ import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSpli import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPlaceholderRuleBuilder; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTfContainer; +import static androidx.window.extensions.embedding.SplitController.KEY_OVERLAY_ASSOCIATE_WITH_LAUNCHING_ACTIVITY; import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_BOTTOM; import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_LEFT; import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_RIGHT; @@ -94,6 +95,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -267,7 +269,7 @@ public class OverlayPresentationTest { } @Test - public void testCreateOrUpdateOverlay_visibleOverlaySameTagInTask_dismissOverlay() { + public void testCreateOrUpdateOverlay_topOverlayInTask_dismissOverlay() { createExistingOverlayContainers(); final TaskFragmentContainer overlayContainer = @@ -295,26 +297,6 @@ public class OverlayPresentationTest { } @Test - public void testCreateOrUpdateOverlay_sameTagTaskAndActivity_updateOverlay() { - createExistingOverlayContainers(); - - final Rect bounds = new Rect(0, 0, 100, 100); - mSplitController.setActivityStackAttributesCalculator(params -> - new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build()); - final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded( - mOverlayContainer1.getOverlayTag()); - - assertWithMessage("overlayContainer1 must be updated since the new overlay container" - + " is launched with the same tag and task") - .that(mSplitController.getAllNonFinishingOverlayContainers()) - .containsExactly(mOverlayContainer1, mOverlayContainer2); - - assertThat(overlayContainer).isEqualTo(mOverlayContainer1); - verify(mSplitPresenter).resizeTaskFragment(eq(mTransaction), - eq(mOverlayContainer1.getTaskFragmentToken()), eq(bounds)); - } - - @Test public void testCreateOrUpdateOverlay_sameTagAndTaskButNotActivity_dismissOverlay() { createExistingOverlayContainers(); @@ -362,6 +344,43 @@ public class OverlayPresentationTest { } @Test + public void testCreateOrUpdateAlwaysOnTopOverlay_dismissMultipleOverlaysInTask() { + createExistingOverlayContainers(); + // Create another overlay in task. + final TaskFragmentContainer overlayContainer3 = + createTestOverlayContainer(TASK_ID, "test3"); + assertThat(mSplitController.getAllNonFinishingOverlayContainers()) + .containsExactly(mOverlayContainer1, mOverlayContainer2, overlayContainer3); + + final TaskFragmentContainer overlayContainer = + createOrUpdateAlwaysOnTopOverlay("test4"); + + assertWithMessage("overlayContainer1 and overlayContainer3 must be dismissed") + .that(mSplitController.getAllNonFinishingOverlayContainers()) + .containsExactly(mOverlayContainer2, overlayContainer); + } + + @Test + public void testCreateOrUpdateAlwaysOnTopOverlay_updateOverlay() { + createExistingOverlayContainers(); + // Create another overlay in task. + final TaskFragmentContainer alwaysOnTopOverlay = createTestOverlayContainer(TASK_ID, + "test3", true /* isVisible */, false /* associateLaunchingActivity */); + final ActivityStackAttributes attrs = new ActivityStackAttributes.Builder() + .setRelativeBounds(new Rect(0, 0, 100, 100)).build(); + mSplitController.setActivityStackAttributesCalculator(params -> attrs); + + Mockito.clearInvocations(mSplitPresenter); + final TaskFragmentContainer overlayContainer = + createOrUpdateAlwaysOnTopOverlay(alwaysOnTopOverlay.getOverlayTag()); + + assertWithMessage("overlayContainer1 and overlayContainer3 must be dismissed") + .that(mSplitController.getAllNonFinishingOverlayContainers()) + .containsExactly(mOverlayContainer2, alwaysOnTopOverlay); + assertThat(overlayContainer).isEqualTo(alwaysOnTopOverlay); + } + + @Test public void testCreateOrUpdateOverlay_launchFromSplit_returnNull() { final Activity primaryActivity = createMockActivity(); final Activity secondaryActivity = createMockActivity(); @@ -381,13 +400,13 @@ public class OverlayPresentationTest { } private void createExistingOverlayContainers() { - createExistingOverlayContainers(true /* visible */); + createExistingOverlayContainers(true /* isOnTop */); } - private void createExistingOverlayContainers(boolean visible) { - mOverlayContainer1 = createTestOverlayContainer(TASK_ID, "test1", visible, + private void createExistingOverlayContainers(boolean isOnTop) { + mOverlayContainer1 = createTestOverlayContainer(TASK_ID, "test1", isOnTop, true /* associatedLaunchingActivity */, mActivity); - mOverlayContainer2 = createTestOverlayContainer(TASK_ID + 1, "test2", visible); + mOverlayContainer2 = createTestOverlayContainer(TASK_ID + 1, "test2", isOnTop); List<TaskFragmentContainer> overlayContainers = mSplitController .getAllNonFinishingOverlayContainers(); assertThat(overlayContainers).containsExactly(mOverlayContainer1, mOverlayContainer2); @@ -966,6 +985,16 @@ public class OverlayPresentationTest { launchOptions, mIntent, activity); } + @Nullable + private TaskFragmentContainer createOrUpdateAlwaysOnTopOverlay( + @NonNull String tag) { + final Bundle launchOptions = new Bundle(); + launchOptions.putBoolean(KEY_OVERLAY_ASSOCIATE_WITH_LAUNCHING_ACTIVITY, false); + launchOptions.putString(KEY_OVERLAY_TAG, tag); + return mSplitController.createOrUpdateOverlayTaskFragmentIfNeeded(mTransaction, + launchOptions, mIntent, createMockActivity()); + } + /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */ @NonNull private TaskFragmentContainer createMockTaskFragmentContainer(@NonNull Activity activity) { @@ -975,10 +1004,10 @@ public class OverlayPresentationTest { /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */ @NonNull private TaskFragmentContainer createMockTaskFragmentContainer( - @NonNull Activity activity, boolean isVisible) { + @NonNull Activity activity, boolean isOnTop) { final TaskFragmentContainer container = createTfContainer(mSplitController, activity.getTaskId(), activity); - setupTaskFragmentInfo(container, activity, isVisible); + setupTaskFragmentInfo(container, activity, isOnTop); return container; } @@ -990,8 +1019,8 @@ public class OverlayPresentationTest { @NonNull private TaskFragmentContainer createTestOverlayContainer(int taskId, @NonNull String tag, - boolean isVisible) { - return createTestOverlayContainer(taskId, tag, isVisible, + boolean isOnTop) { + return createTestOverlayContainer(taskId, tag, isOnTop, true /* associateLaunchingActivity */); } @@ -1002,11 +1031,9 @@ public class OverlayPresentationTest { null /* launchingActivity */); } - // TODO(b/243518738): add more test coverage on overlay container without activity association - // once we have use cases. @NonNull private TaskFragmentContainer createTestOverlayContainer(int taskId, @NonNull String tag, - boolean isVisible, boolean associateLaunchingActivity, + boolean isOnTop, boolean associateLaunchingActivity, @Nullable Activity launchingActivity) { final Activity activity = launchingActivity != null ? launchingActivity : createMockActivity(); @@ -1017,14 +1044,15 @@ public class OverlayPresentationTest { .setLaunchOptions(Bundle.EMPTY) .setAssociatedActivity(associateLaunchingActivity ? activity : null) .build(); - setupTaskFragmentInfo(overlayContainer, createMockActivity(), isVisible); + setupTaskFragmentInfo(overlayContainer, createMockActivity(), isOnTop); return overlayContainer; } private void setupTaskFragmentInfo(@NonNull TaskFragmentContainer container, @NonNull Activity activity, - boolean isVisible) { - final TaskFragmentInfo info = createMockTaskFragmentInfo(container, activity, isVisible); + boolean isOnTop) { + final TaskFragmentInfo info = createMockTaskFragmentInfo(container, activity, isOnTop, + isOnTop); container.setInfo(mTransaction, info); mSplitPresenter.mFragmentInfos.put(container.getTaskFragmentToken(), info); } diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 269a58693a24..606ebb41bc5f 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -554,15 +554,10 @@ enable_windowing_edge_drag_resize is disabled. --> <dimen name="freeform_resize_corner">44dp</dimen> - <!-- The width of the area at the sides of the screen where a freeform task will transition to - split select if dragged until the touch input is within the range. --> - <dimen name="desktop_mode_transition_area_width">32dp</dimen> + <!-- The thickness in dp for all desktop drag transition regions. --> + <dimen name="desktop_mode_transition_region_thickness">44dp</dimen> - <!-- The width of the area where a desktop task will transition to fullscreen. --> - <dimen name="desktop_mode_fullscreen_from_desktop_width">80dp</dimen> - - <!-- The height of the area where a desktop task will transition to fullscreen. --> - <dimen name="desktop_mode_fullscreen_from_desktop_height">40dp</dimen> + <item type="dimen" format="float" name="desktop_mode_fullscreen_region_scale">0.4</item> <!-- The height on the screen where drag to the left or right edge will result in a desktop task snapping to split size. The empty space between this and the top is to allow diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt index 2b01eac3c210..13049694d3fb 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt @@ -43,7 +43,8 @@ enum class DesktopModeFlags( APP_HEADER_WITH_TASK_DENSITY(Flags::enableAppHeaderWithTaskDensity, true), TASK_STACK_OBSERVER_IN_SHELL(Flags::enableTaskStackObserverInShell, true), SIZE_CONSTRAINTS(Flags::enableDesktopWindowingSizeConstraints, true), - DYNAMIC_INITIAL_BOUNDS(Flags::enableWindowingDynamicInitialBounds, true); + DYNAMIC_INITIAL_BOUNDS(Flags::enableWindowingDynamicInitialBounds, true), + ENABLE_DESKTOP_WINDOWING_TASK_LIMIT(Flags::enableDesktopWindowingTaskLimit, true); /** * Determines state of flag based on the actual flag and desktop mode developer option overrides. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 35d387632f03..02625076a7c8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -16,7 +16,7 @@ package com.android.wm.shell.dagger; -import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE; +import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASK_LIMIT; import android.annotation.Nullable; import android.app.KeyguardManager; @@ -569,7 +569,7 @@ public abstract class WMShellModule { ShellTaskOrganizer shellTaskOrganizer) { int maxTaskLimit = DesktopModeStatus.getMaxTaskLimit(context); if (!DesktopModeStatus.canEnterDesktopMode(context) - || !DESKTOP_WINDOWING_MODE.isEnabled(context) + || !ENABLE_DESKTOP_WINDOWING_TASK_LIMIT.isEnabled(context) || maxTaskLimit <= 0) { return Optional.empty(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java index ea7e9685dd92..06c1e68753e1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java @@ -104,6 +104,7 @@ public abstract class Pip2Module { TaskStackListenerImpl taskStackListener, ShellTaskOrganizer shellTaskOrganizer, PipTransitionState pipTransitionState, + PipTouchHandler pipTouchHandler, @ShellMainThread ShellExecutor mainExecutor) { if (!PipUtils.isPip2ExperimentEnabled()) { return Optional.empty(); @@ -112,7 +113,7 @@ public abstract class Pip2Module { context, shellInit, shellCommandHandler, shellController, displayController, displayInsetsController, pipBoundsState, pipBoundsAlgorithm, pipDisplayLayoutState, pipScheduler, taskStackListener, shellTaskOrganizer, - pipTransitionState, mainExecutor)); + pipTransitionState, pipTouchHandler, mainExecutor)); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt index 4299841da0fa..131c5c2697d7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt @@ -250,11 +250,17 @@ class DesktopModeTaskRepository { logD("getVisibleTaskCount=$it") } - /** Adds task (or moves if it already exists) to the top of the ordered list. */ + /** + * Adds task (or moves if it already exists) to the top of the ordered list. + * + * Unminimizes the task if it is minimized. + */ fun addOrMoveFreeformTaskToTop(displayId: Int, taskId: Int) { logD("Add or move task to top: display=%d taskId=%d", taskId, displayId) desktopTaskDataByDisplayId[displayId]?.freeformTasksInZOrder?.remove(taskId) desktopTaskDataByDisplayId.getOrCreate(displayId).freeformTasksInZOrder.add(0, taskId) + // Unminimize the task if it is minimized. + unminimizeTask(displayId, taskId) } /** Minimizes the task for [taskId] and [displayId] */ @@ -270,13 +276,37 @@ class DesktopModeTaskRepository { logW("Unminimize Task: display=%d, task=%d, no task data", displayId, taskId) } - /** Removes task from the ordered list. */ + private fun getDisplayIdForTask(taskId: Int): Int? { + desktopTaskDataByDisplayId.forEach { displayId, data -> + if (taskId in data.freeformTasksInZOrder) { + return displayId + } + } + logW("No display id found for task: taskId=%d", taskId) + return null + } + + /** + * Removes [taskId] from the respective display. If [INVALID_DISPLAY], the original display id + * will be looked up from the task id. + */ fun removeFreeformTask(displayId: Int, taskId: Int) { logD("Removes freeform task: taskId=%d", taskId) + if (displayId == INVALID_DISPLAY) { + // Removes the original display id of the task. + getDisplayIdForTask(taskId)?.let { removeTaskFromDisplay(it, taskId) } + } else { + removeTaskFromDisplay(displayId, taskId) + } + } + + /** Removes given task from a valid [displayId]. */ + private fun removeTaskFromDisplay(displayId: Int, taskId: Int) { + logD("Removes freeform task: taskId=%d, displayId=%d", taskId, displayId) desktopTaskDataByDisplayId[displayId]?.freeformTasksInZOrder?.remove(taskId) boundsBeforeMaximizeByTaskId.remove(taskId) - logD("Remaining freeform tasks: %d", - desktopTaskDataByDisplayId[displayId]?.freeformTasksInZOrder?.toDumpString() ?: "") + logD("Remaining freeform tasks: %s", + desktopTaskDataByDisplayId[displayId]?.freeformTasksInZOrder?.toDumpString()) } /** @@ -358,3 +388,4 @@ class DesktopModeTaskRepository { private fun <T> Iterable<T>.toDumpString(): String = joinToString(separator = ", ", prefix = "[", postfix = "]") + diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java index ed0d2b87b03f..6011db7fc752 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java @@ -105,7 +105,7 @@ public class DesktopModeVisualIndicator { // If we are in freeform, we don't want a visible indicator in the "freeform" drag zone. IndicatorType result = IndicatorType.NO_INDICATOR; final int transitionAreaWidth = mContext.getResources().getDimensionPixelSize( - com.android.wm.shell.R.dimen.desktop_mode_transition_area_width); + com.android.wm.shell.R.dimen.desktop_mode_transition_region_thickness); // Because drags in freeform use task position for indicator calculation, we need to // account for the possibility of the task going off the top of the screen by captionHeight final int captionHeight = mContext.getResources().getDimensionPixelSize( @@ -140,18 +140,19 @@ public class DesktopModeVisualIndicator { final Region region = new Region(); int transitionHeight = windowingMode == WINDOWING_MODE_FREEFORM ? mContext.getResources().getDimensionPixelSize( - com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_height) + com.android.wm.shell.R.dimen.desktop_mode_transition_region_thickness) : 2 * layout.stableInsets().top; - // A thin, short Rect at the top of the screen. + // A Rect at the top of the screen that takes up the center 40%. if (windowingMode == WINDOWING_MODE_FREEFORM) { - int fromFreeformWidth = mContext.getResources().getDimensionPixelSize( - com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_width); - region.union(new Rect((layout.width() / 2) - (fromFreeformWidth / 2), + final float toFullscreenScale = mContext.getResources().getFloat( + R.dimen.desktop_mode_fullscreen_region_scale); + final float toFullscreenWidth = (layout.width() * toFullscreenScale); + region.union(new Rect((int) ((layout.width() / 2f) - (toFullscreenWidth / 2f)), -captionHeight, - (layout.width() / 2) + (fromFreeformWidth / 2), + (int) ((layout.width() / 2f) + (toFullscreenWidth / 2f)), transitionHeight)); } - // A screen-wide, shorter Rect if the task is in fullscreen or split. + // A screen-wide Rect if the task is in fullscreen or split. if (windowingMode == WINDOWING_MODE_FULLSCREEN || windowingMode == WINDOWING_MODE_MULTI_WINDOW) { region.union(new Rect(0, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt index a011ff5636d2..f41d6e3f8dc0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt @@ -33,7 +33,8 @@ import com.android.wm.shell.transition.Transitions.TransitionObserver * Limits the number of tasks shown in Desktop Mode. * * This class should only be used if - * [com.android.window.flags.Flags.enableDesktopWindowingTaskLimit()] is true. + * [com.android.wm.shell.shared.desktopmode.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASK_LIMIT] + * is enabled and [maxTasksLimit] is strictly greater than 0. */ class DesktopTasksLimiter ( transitions: Transitions, @@ -52,6 +53,8 @@ class DesktopTasksLimiter ( } transitions.registerObserver(minimizeTransitionObserver) taskRepository.addActiveTaskListener(leftoverMinimizedTasksRemover) + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "DesktopTasksLimiter: starting limiter with a maximum of %d tasks", maxTasksLimit) } private data class TaskDetails (val displayId: Int, val taskId: Int) @@ -86,10 +89,10 @@ class DesktopTasksLimiter ( } /** - * Returns whether the given Task is being reordered to the back in the given transition, or - * is already invisible. + * Returns whether the Task [taskDetails] is being reordered to the back in the transition + * [info], or is already invisible. * - * <p> This check can be used to double-check that a task was indeed minimized before + * This check can be used to double-check that a task was indeed minimized before * marking it as such. */ private fun isTaskReorderedToBackOrInvisible( @@ -138,7 +141,9 @@ class DesktopTasksLimiter ( } ProtoLog.v( ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, - "DesktopTasksLimiter: removing leftover minimized tasks: $remainingMinimizedTasks") + "DesktopTasksLimiter: removing leftover minimized tasks: %s", + remainingMinimizedTasks, + ) remainingMinimizedTasks.forEach { taskIdToRemove -> val taskToRemove = shellTaskOrganizer.getRunningTaskInfo(taskIdToRemove) if (taskToRemove != null) { @@ -149,8 +154,8 @@ class DesktopTasksLimiter ( } /** - * Mark a task as minimized, this should only be done after the corresponding transition has - * finished so we don't minimize the task if the transition fails. + * Mark [taskId], which must be on [displayId], as minimized, this should only be done after the + * corresponding transition has finished so we don't minimize the task if the transition fails. */ private fun markTaskMinimized(displayId: Int, taskId: Int) { ProtoLog.v( @@ -161,11 +166,9 @@ class DesktopTasksLimiter ( /** * Add a minimize-transition to [wct] if adding [newFrontTaskInfo] brings us over the task - * limit. + * limit, returning the task to minimize. * - * @param transition the transition that the minimize-transition will be appended to, or null if - * the transition will be started later. - * @return the ID of the minimized task, or null if no task is being minimized. + * The task must be on [displayId]. */ fun addAndGetMinimizeTaskChangesIfNeeded( displayId: Int, @@ -220,13 +223,15 @@ class DesktopTasksLimiter ( // No need to minimize anything return null } + val taskIdToMinimize = visibleFreeformTaskIdsOrderedFrontToBack.last() val taskToMinimize = - shellTaskOrganizer.getRunningTaskInfo( - visibleFreeformTaskIdsOrderedFrontToBack.last()) + shellTaskOrganizer.getRunningTaskInfo(taskIdToMinimize) if (taskToMinimize == null) { ProtoLog.e( ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, - "DesktopTasksLimiter: taskToMinimize == null") + "DesktopTasksLimiter: taskToMinimize(taskId = %d) == null", + taskIdToMinimize, + ) return null } return taskToMinimize diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java index 229d972b2834..2931ef38d857 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java @@ -99,7 +99,6 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, if (DesktopModeStatus.canEnterDesktopMode(mContext)) { mDesktopModeTaskRepository.ifPresent(repository -> { repository.addOrMoveFreeformTaskToTop(taskInfo.displayId, taskInfo.taskId); - repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId); if (taskInfo.isVisible) { repository.addActiveTask(taskInfo.displayId, taskInfo.taskId); repository.updateVisibleFreeformTasks(taskInfo.displayId, taskInfo.taskId, @@ -161,7 +160,6 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, if (DesktopModeStatus.canEnterDesktopMode(mContext) && taskInfo.isFocused) { mDesktopModeTaskRepository.ifPresent(repository -> { repository.addOrMoveFreeformTaskToTop(taskInfo.displayId, taskInfo.taskId); - repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId); }); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index 7451d2251588..284620e7d0c4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -272,6 +272,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb final boolean changed = onDisplayRotationChanged(mContext, outBounds, currentBounds, mTmpInsetBounds, displayId, fromRotation, toRotation, t); if (changed) { + mMenuController.hideMenu(); // If the pip was in the offset zone earlier, adjust the new bounds to the bottom of the // movement bounds mTouchHandler.adjustBoundsForRotation(outBounds, mPipBoundsState.getBounds(), diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java index 8aa093379ee7..94fe286de869 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java @@ -88,6 +88,7 @@ public class PipController implements ConfigurationChangeListener, private final TaskStackListenerImpl mTaskStackListener; private final ShellTaskOrganizer mShellTaskOrganizer; private final PipTransitionState mPipTransitionState; + private final PipTouchHandler mPipTouchHandler; private final ShellExecutor mMainExecutor; private final PipImpl mImpl; private Consumer<Boolean> mOnIsInPipStateChangedListener; @@ -130,6 +131,7 @@ public class PipController implements ConfigurationChangeListener, TaskStackListenerImpl taskStackListener, ShellTaskOrganizer shellTaskOrganizer, PipTransitionState pipTransitionState, + PipTouchHandler pipTouchHandler, ShellExecutor mainExecutor) { mContext = context; mShellCommandHandler = shellCommandHandler; @@ -144,6 +146,7 @@ public class PipController implements ConfigurationChangeListener, mShellTaskOrganizer = shellTaskOrganizer; mPipTransitionState = pipTransitionState; mPipTransitionState.addPipTransitionStateChangedListener(this); + mPipTouchHandler = pipTouchHandler; mMainExecutor = mainExecutor; mImpl = new PipImpl(); @@ -168,6 +171,7 @@ public class PipController implements ConfigurationChangeListener, TaskStackListenerImpl taskStackListener, ShellTaskOrganizer shellTaskOrganizer, PipTransitionState pipTransitionState, + PipTouchHandler pipTouchHandler, ShellExecutor mainExecutor) { if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, @@ -177,7 +181,7 @@ public class PipController implements ConfigurationChangeListener, return new PipController(context, shellInit, shellCommandHandler, shellController, displayController, displayInsetsController, pipBoundsState, pipBoundsAlgorithm, pipDisplayLayoutState, pipScheduler, taskStackListener, shellTaskOrganizer, - pipTransitionState, mainExecutor); + pipTransitionState, pipTouchHandler, mainExecutor); } public PipImpl getPipImpl() { @@ -204,7 +208,9 @@ public class PipController implements ConfigurationChangeListener, mDisplayInsetsController.addInsetsChangedListener(mPipDisplayLayoutState.getDisplayId(), new ImeListener(mDisplayController, mPipDisplayLayoutState.getDisplayId()) { @Override - public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {} + public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { + mPipTouchHandler.onImeVisibilityChanged(imeVisible, imeHeight); + } }); // Allow other outside processes to bind to PiP controller using the key below. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java index e1e072a7faad..83253c6006fb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java @@ -134,6 +134,8 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, private final PhysicsAnimator.SpringConfig mConflictResolutionSpringConfig = new PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_NO_BOUNCY); + @Nullable private Runnable mUpdateMovementBoundsRunnable; + private final Consumer<Rect> mUpdateBoundsCallback = (Rect newBounds) -> { if (mPipBoundsState.getBounds().equals(newBounds)) { return; @@ -141,6 +143,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, mMenuController.updateMenuLayout(newBounds); mPipBoundsState.setBounds(newBounds); + maybeUpdateMovementBounds(); }; /** @@ -566,11 +569,20 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, + " callers=\n%s", TAG, originalBounds, offset, Debug.getCallers(5, " ")); } + if (offset == 0) { + return; + } + cancelPhysicsAnimation(); - /* - mPipTaskOrganizer.scheduleOffsetPip(originalBounds, offset, SHIFT_DURATION, - mUpdateBoundsCallback); - */ + + Rect adjustedBounds = new Rect(originalBounds); + adjustedBounds.offset(0, offset); + + setAnimatingToBounds(adjustedBounds); + Bundle extra = new Bundle(); + extra.putBoolean(ANIMATING_BOUNDS_CHANGE, true); + extra.putInt(ANIMATING_BOUNDS_CHANGE_DURATION, SHIFT_DURATION); + mPipTransitionState.setState(PipTransitionState.SCHEDULED_BOUNDS_CHANGE, extra); } /** @@ -585,11 +597,11 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, /** Set new fling configs whose min/max values respect the given movement bounds. */ private void rebuildFlingConfigs() { mFlingConfigX = new PhysicsAnimator.FlingConfig(DEFAULT_FRICTION, - mPipBoundsAlgorithm.getMovementBounds(getBounds()).left, - mPipBoundsAlgorithm.getMovementBounds(getBounds()).right); + mPipBoundsState.getMovementBounds().left, + mPipBoundsState.getMovementBounds().right); mFlingConfigY = new PhysicsAnimator.FlingConfig(DEFAULT_FRICTION, - mPipBoundsAlgorithm.getMovementBounds(getBounds()).top, - mPipBoundsAlgorithm.getMovementBounds(getBounds()).bottom); + mPipBoundsState.getMovementBounds().top, + mPipBoundsState.getMovementBounds().bottom); final Rect insetBounds = mPipBoundsState.getDisplayLayout().stableInsets(); mStashConfigX = new PhysicsAnimator.FlingConfig( DEFAULT_FRICTION, @@ -671,6 +683,16 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, cleanUpHighPerfSessionMaybe(); } + void setUpdateMovementBoundsRunnable(Runnable updateMovementBoundsRunnable) { + mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable; + } + + private void maybeUpdateMovementBounds() { + if (mUpdateMovementBoundsRunnable != null) { + mUpdateMovementBoundsRunnable.run(); + } + } + /** * Notifies the floating coordinator that we're moving, and sets the animating to bounds so * we return these bounds from @@ -807,8 +829,14 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, startTx, finishTx, mPipBoundsState.getBounds(), mPipBoundsState.getBounds(), destinationBounds, duration, 0f /* angle */); animator.setAnimationEndCallback(() -> { - mPipBoundsState.setBounds(destinationBounds); - // All motion operations have actually finished, so make bounds cache updates. + mUpdateBoundsCallback.accept(destinationBounds); + + // In case an ongoing drag/fling was present before a deterministic resize transition + // kicked in, we need to update the update bounds properly before cleaning in-motion + // state. + mPipBoundsState.getMotionBoundsState().setBoundsInMotion(destinationBounds); + settlePipBoundsAfterPhysicsAnimation(false /* animatingAfter */); + cleanUpHighPerfSessionMaybe(); // Signal that we are done with resize transition mPipScheduler.scheduleFinishResizePip(true /* configAtEnd */); @@ -817,7 +845,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, } private void settlePipBoundsAfterPhysicsAnimation(boolean animatingAfter) { - if (!animatingAfter) { + if (!animatingAfter && mPipBoundsState.getMotionBoundsState().isInMotion()) { // The physics animation ended, though we may not necessarily be done animating, such as // when we're still dragging after moving out of the magnetic target. Only set the final // bounds state and clear motion bounds completely if the whole animation is over. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java index 5b0ca1837a1c..d28204add0ac 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java @@ -146,8 +146,8 @@ public class PipResizeGestureHandler implements mUpdateResizeBoundsCallback = (rect) -> { mUserResizeBounds.set(rect); // mMotionHelper.synchronizePinnedStackBounds(); - mUpdateMovementBoundsRunnable.run(); mPipBoundsState.setBounds(rect); + mUpdateMovementBoundsRunnable.run(); resetState(); }; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java index 53b80e8b7542..f387e72b3da6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java @@ -199,6 +199,7 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha mMenuController.addListener(new PipMenuListener()); mGesture = new DefaultPipTouchGesture(); mMotionHelper = pipMotionHelper; + mMotionHelper.setUpdateMovementBoundsRunnable(this::updateMovementBounds); mPipDismissTargetHandler = new PipDismissTargetHandler(context, pipUiEventLogger, mMotionHelper, mainExecutor); mTouchState = new PipTouchState(ViewConfiguration.get(context), @@ -317,6 +318,8 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha mFloatingContentCoordinator.onContentRemoved(mMotionHelper); mPipResizeGestureHandler.onActivityUnpinned(); mPipInputConsumer.unregisterInputConsumer(); + mPipBoundsState.setHasUserMovedPip(false); + mPipBoundsState.setHasUserResizedPip(false); } void onPinnedStackAnimationEnded( @@ -346,6 +349,22 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { mIsImeShowing = imeVisible; mImeHeight = imeHeight; + + // Cache new movement bounds using the new potential IME height. + updateMovementBounds(); + + mPipTransitionState.setOnIdlePipTransitionStateRunnable(() -> { + int delta = mPipBoundsState.getMovementBounds().bottom + - mPipBoundsState.getBounds().top; + + boolean hasUserInteracted = (mPipBoundsState.hasUserMovedPip() + || mPipBoundsState.hasUserResizedPip()); + if ((imeVisible && delta < 0) || (!imeVisible && !hasUserInteracted)) { + // The policy is to ignore an IME disappearing if user has interacted with PiP. + // Otherwise, only offset due to an appearing IME if PiP occludes it. + mMotionHelper.animateToOffset(mPipBoundsState.getBounds(), delta); + } + }); } void onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight) { @@ -1077,6 +1096,7 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha switch (newState) { case PipTransitionState.ENTERED_PIP: onActivityPinned(); + updateMovementBounds(); mTouchState.setAllowInputEvents(true); mTouchState.reset(); break; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java index 29272be6e9bd..a132796f4a84 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java @@ -149,6 +149,12 @@ public class PipTransitionState { @Nullable private SurfaceControl mSwipePipToHomeOverlay; + // + // Scheduling-related state + // + @Nullable + private Runnable mOnIdlePipTransitionStateRunnable; + /** * An interface to track state updates as we progress through PiP transitions. */ @@ -197,6 +203,8 @@ public class PipTransitionState { mState = state; dispatchPipTransitionStateChanged(prevState, mState, extra); } + + maybeRunOnIdlePipTransitionStateCallback(); } /** @@ -231,6 +239,29 @@ public class PipTransitionState { } /** + * Schedule a callback to run when in a valid idle PiP state. + * + * <p>We only allow for one callback to be scheduled to avoid cases with multiple transitions + * being scheduled. For instance, if user double taps and IME shows, this would + * schedule a bounds change transition for IME appearing. But if some other transition would + * want to animate PiP before the scheduled callback executes, we would rather want to replace + * the existing callback with a new one, to avoid multiple animations + * as soon as we are idle.</p> + */ + public void setOnIdlePipTransitionStateRunnable( + @Nullable Runnable onIdlePipTransitionStateRunnable) { + mOnIdlePipTransitionStateRunnable = onIdlePipTransitionStateRunnable; + maybeRunOnIdlePipTransitionStateCallback(); + } + + private void maybeRunOnIdlePipTransitionStateCallback() { + if (mOnIdlePipTransitionStateRunnable != null && isPipStateIdle()) { + mOnIdlePipTransitionStateRunnable.run(); + mOnIdlePipTransitionStateRunnable = null; + } + } + + /** * Adds a {@link PipTransitionStateChangedListener} for future PiP transition state updates. */ public void addPipTransitionStateChangedListener(PipTransitionStateChangedListener listener) { @@ -318,6 +349,11 @@ public class PipTransitionState { throw new IllegalStateException("Unknown state: " + state); } + public boolean isPipStateIdle() { + // This needs to be a valid in-PiP state that isn't a transient state. + return mState == ENTERED_PIP || mState == CHANGED_PIP_BOUNDS; + } + @Override public String toString() { return String.format("PipTransitionState(mState=%s, mInSwipePipToHomeTransition=%b)", diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt index 0a5672d45f33..7672a8fd011b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt @@ -468,8 +468,65 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { } @Test + fun addOrMoveFreeformTaskToTop_taskIsMinimized_unminimizesTask() { + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 5) + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6) + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 7) + repo.minimizeTask(displayId = 0, taskId = 6) + + val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY) + assertThat(tasks).containsExactly(7, 6, 5).inOrder() + assertThat(repo.isMinimizedTask(taskId = 6)).isTrue() + } + + @Test + fun addOrMoveFreeformTaskToTop_taskIsUnminimized_noop() { + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 5) + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6) + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 7) + + val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY) + assertThat(tasks).containsExactly(7, 6, 5).inOrder() + assertThat(repo.isMinimizedTask(taskId = 6)).isFalse() + } + + @Test + fun removeFreeformTask_invalidDisplay_removesTaskFromFreeformTasks() { + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1) + + repo.removeFreeformTask(INVALID_DISPLAY, taskId = 1) + + val invalidDisplayTasks = repo.getFreeformTasksInZOrder(INVALID_DISPLAY) + assertThat(invalidDisplayTasks).isEmpty() + val validDisplayTasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY) + assertThat(validDisplayTasks).isEmpty() + } + + @Test + fun removeFreeformTask_validDisplay_removesTaskFromFreeformTasks() { + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1) + + repo.removeFreeformTask(DEFAULT_DISPLAY, taskId = 1) + + val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY) + assertThat(tasks).isEmpty() + } + + @Test + fun removeFreeformTask_validDisplay_differentDisplay_doesNotRemovesTask() { + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1) + + repo.removeFreeformTask(SECOND_DISPLAY, taskId = 1) + + val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY) + assertThat(tasks).containsExactly(1) + } + + @Test fun removeFreeformTask_removesTaskBoundsBeforeMaximize() { val taskId = 1 + repo.addActiveTask(THIRD_DISPLAY, taskId) + repo.addOrMoveFreeformTaskToTop(THIRD_DISPLAY, taskId) repo.saveBoundsBeforeMaximize(taskId, Rect(0, 0, 200, 200)) repo.removeFreeformTask(THIRD_DISPLAY, taskId) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt index bd39aa6ace42..2dea43b508ae 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt @@ -61,20 +61,23 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { @Test fun testFullscreenRegionCalculation() { - val transitionHeight = context.resources.getDimensionPixelSize( - R.dimen.desktop_mode_fullscreen_from_desktop_height) - val fromFreeformWidth = mContext.resources.getDimensionPixelSize( - R.dimen.desktop_mode_fullscreen_from_desktop_width - ) var testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, WINDOWING_MODE_FULLSCREEN, CAPTION_HEIGHT) assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 2400, 2 * STABLE_INSETS.top)) testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, WINDOWING_MODE_FREEFORM, CAPTION_HEIGHT) + + val transitionHeight = context.resources.getDimensionPixelSize( + R.dimen.desktop_mode_transition_region_thickness) + val toFullscreenScale = mContext.resources.getFloat( + R.dimen.desktop_mode_fullscreen_region_scale + ) + val toFullscreenWidth = displayLayout.width() * toFullscreenScale + assertThat(testRegion.bounds).isEqualTo(Rect( - DISPLAY_BOUNDS.width() / 2 - fromFreeformWidth / 2, + (DISPLAY_BOUNDS.width() / 2f - toFullscreenWidth / 2f).toInt(), -50, - DISPLAY_BOUNDS.width() / 2 + fromFreeformWidth / 2, + (DISPLAY_BOUNDS.width() / 2f + toFullscreenWidth / 2f).toInt(), transitionHeight)) testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, WINDOWING_MODE_MULTI_WINDOW, CAPTION_HEIGHT) diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index e302fa8b1fc3..d71f3b6884ae 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -736,6 +736,7 @@ cc_defaults { cc_test { name: "hwui_unit_tests", + test_config: "tests/unit/AndroidTest.xml", defaults: [ "hwui_test_defaults", "android_graphics_apex", @@ -803,6 +804,7 @@ cc_test { cc_benchmark { name: "hwuimacro", + test_config: "tests/macrobench/AndroidTest.xml", defaults: ["hwui_test_defaults"], static_libs: ["libhwui"], @@ -822,6 +824,7 @@ cc_benchmark { cc_benchmark { name: "hwuimicro", + test_config: "tests/microbench/AndroidTest.xml", defaults: ["hwui_test_defaults"], static_libs: ["libhwui_static"], diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp index 5d3bc89b40dd..d184f64b1c2c 100644 --- a/libs/hwui/Properties.cpp +++ b/libs/hwui/Properties.cpp @@ -101,6 +101,8 @@ float Properties::maxHdrHeadroomOn8bit = 5.f; // TODO: Refine this number bool Properties::clipSurfaceViews = false; bool Properties::hdr10bitPlus = false; +int Properties::timeoutMultiplier = 1; + StretchEffectBehavior Properties::stretchEffectBehavior = StretchEffectBehavior::ShaderHWUI; DrawingEnabled Properties::drawingEnabled = DrawingEnabled::NotInitialized; @@ -174,6 +176,8 @@ bool Properties::load() { base::GetBoolProperty("debug.hwui.clip_surfaceviews", hwui_flags::clip_surfaceviews()); hdr10bitPlus = hwui_flags::hdr_10bit_plus(); + timeoutMultiplier = android::base::GetIntProperty("ro.hw_timeout_multiplier", 1); + return (prevDebugLayersUpdates != debugLayersUpdates) || (prevDebugOverdraw != debugOverdraw); } diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h index d3176f6879d2..e2646422030e 100644 --- a/libs/hwui/Properties.h +++ b/libs/hwui/Properties.h @@ -343,6 +343,8 @@ public: static bool clipSurfaceViews; static bool hdr10bitPlus; + static int timeoutMultiplier; + static StretchEffectBehavior getStretchEffectBehavior() { return stretchEffectBehavior; } diff --git a/libs/hwui/Readback.cpp b/libs/hwui/Readback.cpp index afe4c3896ed2..2f15722a23e0 100644 --- a/libs/hwui/Readback.cpp +++ b/libs/hwui/Readback.cpp @@ -91,8 +91,10 @@ void Readback::copySurfaceInto(ANativeWindow* window, const std::shared_ptr<Copy { ATRACE_NAME("sync_wait"); - if (sourceFence != -1 && sync_wait(sourceFence.get(), 500 /* ms */) != NO_ERROR) { - ALOGE("Timeout (500ms) exceeded waiting for buffer fence, abandoning readback attempt"); + int syncWaitTimeoutMs = 500 * Properties::timeoutMultiplier; + if (sourceFence != -1 && sync_wait(sourceFence.get(), syncWaitTimeoutMs) != NO_ERROR) { + ALOGE("Timeout (%dms) exceeded waiting for buffer fence, abandoning readback attempt", + syncWaitTimeoutMs); return request->onCopyFinished(CopyResult::Timeout); } } @@ -109,9 +111,8 @@ void Readback::copySurfaceInto(ANativeWindow* window, const std::shared_ptr<Copy sk_sp<SkColorSpace> colorSpace = DataSpaceToColorSpace(static_cast<android_dataspace>(dataspace)); - sk_sp<SkImage> image = - SkImages::DeferredFromAHardwareBuffer(sourceBuffer.get(), kPremul_SkAlphaType, - colorSpace); + sk_sp<SkImage> image = SkImages::DeferredFromAHardwareBuffer(sourceBuffer.get(), + kPremul_SkAlphaType, colorSpace); if (!image.get()) { return request->onCopyFinished(CopyResult::UnknownError); diff --git a/libs/hwui/tests/macrobench/AndroidTest.xml b/libs/hwui/tests/macrobench/AndroidTest.xml new file mode 100644 index 000000000000..5b8576d444cd --- /dev/null +++ b/libs/hwui/tests/macrobench/AndroidTest.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<configuration description="Config for hwuimacro"> + <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> + <option name="cleanup" value="true" /> + <option name="push" value="hwuimacro->/data/local/tmp/benchmarktest/hwuimacro" /> + </target_preparer> + <option name="test-suite-tag" value="apct" /> + <option name="not-shardable" value="true" /> + <test class="com.android.tradefed.testtype.GoogleBenchmarkTest" > + <option name="native-benchmark-device-path" value="/data/local/tmp/benchmarktest" /> + <option name="benchmark-module-name" value="hwuimacro" /> + <option name="file-exclusion-filter-regex" value=".*\.config$" /> + </test> +</configuration> diff --git a/libs/hwui/tests/macrobench/how_to_run.txt b/libs/hwui/tests/macrobench/how_to_run.txt index 3c3d36a8977f..59ef25a3aacc 100644 --- a/libs/hwui/tests/macrobench/how_to_run.txt +++ b/libs/hwui/tests/macrobench/how_to_run.txt @@ -3,3 +3,7 @@ adb push $OUT/data/benchmarktest/hwuimacro/hwuimacro /data/benchmarktest/hwuimac adb shell /data/benchmarktest/hwuimacro/hwuimacro shadowgrid2 --onscreen Pass --help to get help + +OR (if you don't need to pass arguments) + +atest hwuimacro diff --git a/libs/hwui/AndroidTest.xml b/libs/hwui/tests/microbench/AndroidTest.xml index 75f61f5f7f9d..d67305dfa323 100644 --- a/libs/hwui/AndroidTest.xml +++ b/libs/hwui/tests/microbench/AndroidTest.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2017 The Android Open Source Project +<!-- 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. @@ -16,24 +16,13 @@ <configuration description="Config for hwuimicro"> <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> <option name="cleanup" value="true" /> - <option name="push" value="hwui_unit_tests->/data/local/tmp/nativetest/hwui_unit_tests" /> <option name="push" value="hwuimicro->/data/local/tmp/benchmarktest/hwuimicro" /> - <option name="push" value="hwuimacro->/data/local/tmp/benchmarktest/hwuimacro" /> </target_preparer> <option name="test-suite-tag" value="apct" /> <option name="not-shardable" value="true" /> - <test class="com.android.tradefed.testtype.GTest" > - <option name="native-test-device-path" value="/data/local/tmp/nativetest" /> - <option name="module-name" value="hwui_unit_tests" /> - </test> <test class="com.android.tradefed.testtype.GoogleBenchmarkTest" > <option name="native-benchmark-device-path" value="/data/local/tmp/benchmarktest" /> <option name="benchmark-module-name" value="hwuimicro" /> <option name="file-exclusion-filter-regex" value=".*\.config$" /> </test> - <test class="com.android.tradefed.testtype.GoogleBenchmarkTest" > - <option name="native-benchmark-device-path" value="/data/local/tmp/benchmarktest" /> - <option name="benchmark-module-name" value="hwuimacro" /> - <option name="file-exclusion-filter-regex" value=".*\.config$" /> - </test> </configuration> diff --git a/libs/hwui/tests/microbench/how_to_run.txt b/libs/hwui/tests/microbench/how_to_run.txt index 915fe5d959f9..c7ddc1a70cd7 100755 --- a/libs/hwui/tests/microbench/how_to_run.txt +++ b/libs/hwui/tests/microbench/how_to_run.txt @@ -1,3 +1,7 @@ mmm -j8 frameworks/base/libs/hwui && adb push $OUT/data/benchmarktest/hwuimicro/hwuimicro /data/benchmarktest/hwuimicro/hwuimicro && adb shell /data/benchmarktest/hwuimicro/hwuimicro + +OR + +atest hwuimicro diff --git a/libs/hwui/tests/unit/AndroidTest.xml b/libs/hwui/tests/unit/AndroidTest.xml new file mode 100644 index 000000000000..dc586c9b740c --- /dev/null +++ b/libs/hwui/tests/unit/AndroidTest.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<configuration description="Config for hwui_unit_tests"> + <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> + <option name="cleanup" value="true" /> + <option name="push" value="hwui_unit_tests->/data/local/tmp/nativetest/hwui_unit_tests" /> + </target_preparer> + <option name="test-suite-tag" value="apct" /> + <option name="not-shardable" value="true" /> + <test class="com.android.tradefed.testtype.GTest" > + <option name="native-test-device-path" value="/data/local/tmp/nativetest" /> + <option name="module-name" value="hwui_unit_tests" /> + </test> +</configuration> diff --git a/libs/hwui/tests/unit/how_to_run.txt b/libs/hwui/tests/unit/how_to_run.txt index c11d6eb33358..1a35adf6b11b 100755 --- a/libs/hwui/tests/unit/how_to_run.txt +++ b/libs/hwui/tests/unit/how_to_run.txt @@ -2,3 +2,11 @@ mmm -j8 frameworks/base/libs/hwui && adb push $ANDROID_PRODUCT_OUT/data/nativetest/hwui_unit_tests/hwui_unit_tests \ /data/nativetest/hwui_unit_tests/hwui_unit_tests && adb shell /data/nativetest/hwui_unit_tests/hwui_unit_tests + +OR + +atest hwui_unit_tests + +OR, if you need arguments, they can be passed as native-test-flags, as in: + +atest hwui_unit_tests -- --test-arg com.android.tradefed.testtype.GTest:native-test-flag:"--renderer=skiavk" diff --git a/libs/hwui/tests/unit/main.cpp b/libs/hwui/tests/unit/main.cpp index 76cbc8abc808..3fd15c4c9c51 100644 --- a/libs/hwui/tests/unit/main.cpp +++ b/libs/hwui/tests/unit/main.cpp @@ -15,6 +15,7 @@ */ #include <getopt.h> +#include <log/log.h> #include <signal.h> #include "Properties.h" @@ -65,6 +66,19 @@ static RenderPipelineType parseRenderer(const char* renderer) { return RenderPipelineType::SkiaGL; } +static constexpr const char* renderPipelineTypeName(const RenderPipelineType renderPipelineType) { + switch (renderPipelineType) { + case RenderPipelineType::SkiaGL: + return "SkiaGL"; + case RenderPipelineType::SkiaVulkan: + return "SkiaVulkan"; + case RenderPipelineType::SkiaCpu: + return "SkiaCpu"; + case RenderPipelineType::NotInitialized: + return "NotInitialized"; + } +} + struct Options { RenderPipelineType renderer = RenderPipelineType::SkiaGL; }; @@ -118,6 +132,7 @@ int main(int argc, char* argv[]) { auto opts = parseOptions(argc, argv); Properties::overrideRenderPipelineType(opts.renderer); + ALOGI("Starting HWUI unit tests with %s pipeline", renderPipelineTypeName(opts.renderer)); // Run the tests testing::InitGoogleTest(&argc, argv); diff --git a/media/java/android/media/AudioAttributes.java b/media/java/android/media/AudioAttributes.java index 42175628a45d..1024a5519965 100644 --- a/media/java/android/media/AudioAttributes.java +++ b/media/java/android/media/AudioAttributes.java @@ -578,6 +578,8 @@ public final class AudioAttributes implements Parcelable { }); private AudioAttributes() { + mBundle = null; + mFormattedTags = ""; } /** diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java index 395f81d73351..0ffab4ba3eaf 100644 --- a/nfc/java/android/nfc/NfcAdapter.java +++ b/nfc/java/android/nfc/NfcAdapter.java @@ -1166,10 +1166,11 @@ public final class NfcAdapter { /** - * Returns whether the device supports observer mode or not. When observe - * mode is enabled, the NFC hardware will listen for NFC readers, but not - * respond to them. When observe mode is disabled, the NFC hardware will - * resoond to the reader and proceed with the transaction. + * Returns whether the device supports observe mode or not. When observe mode is enabled, the + * NFC hardware will listen to NFC readers, but not respond to them. While enabled, observed + * polling frames will be sent to the APDU service (see {@link #setObserveModeEnabled(boolean)}. + * When observe mode is disabled (or if it's not supported), the NFC hardware will automatically + * respond to the reader and proceed with the transaction. * @return true if the mode is supported, false otherwise. */ @FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE) @@ -1193,9 +1194,10 @@ public final class NfcAdapter { * and simply observe and notify the APDU service of polling loop frames. See * {@link #isObserveModeSupported()} for a description of observe mode. Only the package of the * currently preferred service (the service set as preferred by the current foreground - * application via {@link CardEmulation#setPreferredService(Activity, ComponentName)} or the - * current Default Wallet Role Holder {@link android.app.role.RoleManager#ROLE_WALLET}), - * otherwise a call to this method will fail and return false. + * application via {@link android.nfc.cardemulation.CardEmulation#setPreferredService(Activity, + * android.content.ComponentName)} or the current Default Wallet Role Holder + * {@link android.app.role.RoleManager#ROLE_WALLET}), otherwise a call to this method will fail + * and return false. * * @param enabled false disables observe mode to allow the transaction to proceed while true * enables observe mode and does not allow transactions to proceed. diff --git a/packages/InputDevices/res/raw/keyboard_layout_arabic.kcm b/packages/InputDevices/res/raw/keyboard_layout_arabic.kcm index 9c2064c9b8ea..8c6880b111a5 100644 --- a/packages/InputDevices/res/raw/keyboard_layout_arabic.kcm +++ b/packages/InputDevices/res/raw/keyboard_layout_arabic.kcm @@ -58,7 +58,8 @@ key 5 { label: '5' base: '\u0665' capslock: '5' - shift: '%' + shift: '\u066a' + shift+capslock: '%' } key 6 { diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index ce997bf15b91..5c4cdb271a2f 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -1010,8 +1010,8 @@ <!-- UI debug setting: force allow on external summary [CHAR LIMIT=150] --> <string name="force_resizable_activities_summary">Make all activities resizable for multi-window, regardless of manifest values.</string> - <!-- Title for a toggle that enables support for windows to be in freeform (apps run in resizable windows). [CHAR LIMIT=50] --> - <string name="enable_freeform_support">Enable freeform window support</string> + <!-- Title for a toggle that enables support for windows to be in freeform. Freeform windows enables users to freely arrange and resize overlapping apps. [CHAR LIMIT=50] --> + <string name="enable_freeform_support">Enable freeform windows</string> <!-- Local (desktop) backup password menu title [CHAR LIMIT=25] --> <string name="local_backup_password_title">Desktop backup password</string> @@ -1164,7 +1164,7 @@ <!-- [CHAR_LIMIT=40] Label for battery level chart when charging with duration --> <string name="power_charging_duration"><xliff:g id="level">%1$s</xliff:g> - <xliff:g id="time">%2$s</xliff:g> left until full</string> <!-- [CHAR_LIMIT=80] Label for battery level chart when charge been limited --> - <string name="power_charging_limited"><xliff:g id="level">%1$s</xliff:g> - Charging optimized</string> + <string name="power_charging_limited"><xliff:g id="level">%1$s</xliff:g> - Charging on hold to protect battery</string> <!-- [CHAR_LIMIT=80] Label for battery charging future pause --> <string name="power_charging_future_paused"><xliff:g id="level">%1$s</xliff:g> - Charging</string> diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingModel.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingModel.kt new file mode 100644 index 000000000000..db782803937c --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingModel.kt @@ -0,0 +1,62 @@ +/* + * 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.settingslib.bluetooth.devicesettings.shared.model + +import android.content.Intent +import android.graphics.Bitmap +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId + +/** Models a device setting. */ +sealed interface DeviceSettingModel { + val cachedDevice: CachedBluetoothDevice + @DeviceSettingId val id: Int + + /** Models a device setting which should be displayed as an action/switch preference. */ + data class ActionSwitchPreference( + override val cachedDevice: CachedBluetoothDevice, + @DeviceSettingId override val id: Int, + val title: String, + val summary: String? = null, + val icon: Bitmap? = null, + val intent: Intent? = null, + val switchState: DeviceSettingStateModel.ActionSwitchPreferenceState? = null, + val isAllowedChangingState: Boolean = true, + val updateState: ((DeviceSettingStateModel.ActionSwitchPreferenceState) -> Unit)? = null, + ) : DeviceSettingModel + + /** Models a device setting which should be displayed as a multi-toggle preference. */ + data class MultiTogglePreference( + override val cachedDevice: CachedBluetoothDevice, + @DeviceSettingId override val id: Int, + val title: String, + val toggles: List<ToggleModel>, + val isActive: Boolean, + val state: DeviceSettingStateModel.MultiTogglePreferenceState, + val isAllowedChangingState: Boolean, + val updateState: (DeviceSettingStateModel.MultiTogglePreferenceState) -> Unit + ) : DeviceSettingModel + + /** Models an unknown preference. */ + data class Unknown( + override val cachedDevice: CachedBluetoothDevice, + @DeviceSettingId override val id: Int + ) : DeviceSettingModel +} + +/** Models a toggle in [DeviceSettingModel.MultiTogglePreference]. */ +data class ToggleModel(val label: String, val icon: Bitmap) diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingStateModel.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingStateModel.kt new file mode 100644 index 000000000000..b404bb9be682 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingStateModel.kt @@ -0,0 +1,40 @@ +/* + * 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.settingslib.bluetooth.devicesettings.shared.model + +import com.android.settingslib.bluetooth.devicesettings.DeviceSettingPreferenceState + +/** Models a device setting state. */ +sealed interface DeviceSettingStateModel { + fun toParcelable(): DeviceSettingPreferenceState + + /** Models a device setting state for action/switch preference. */ + data class ActionSwitchPreferenceState(val checked: Boolean) : DeviceSettingStateModel { + override fun toParcelable() = + com.android.settingslib.bluetooth.devicesettings.ActionSwitchPreferenceState.Builder() + .setChecked(checked) + .build() + } + + /** Models a device setting state for multi-toggle preference. */ + data class MultiTogglePreferenceState(val selectedIndex: Int) : DeviceSettingStateModel { + override fun toParcelable() = + com.android.settingslib.bluetooth.devicesettings.MultiTogglePreferenceState.Builder() + .setState(selectedIndex) + .build() + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java index 732b358dd243..88af7ee3a54f 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java @@ -158,6 +158,11 @@ public class ZenMode implements Parcelable { mIsManualDnd = isManualDnd; } + /** Creates a deep copy of this object. */ + public ZenMode copy() { + return new ZenMode(mId, new AutomaticZenRule.Builder(mRule).build(), mStatus, mIsManualDnd); + } + @NonNull public String getId() { return mId; diff --git a/packages/SettingsLib/src/com/android/settingslib/satellite/SatelliteDialogUtils.kt b/packages/SettingsLib/src/com/android/settingslib/satellite/SatelliteDialogUtils.kt index d69c87b318e2..2dc2650c9001 100644 --- a/packages/SettingsLib/src/com/android/settingslib/satellite/SatelliteDialogUtils.kt +++ b/packages/SettingsLib/src/com/android/settingslib/satellite/SatelliteDialogUtils.kt @@ -21,6 +21,7 @@ import android.content.Context import android.content.Intent import android.os.OutcomeReceiver import android.telephony.satellite.SatelliteManager +import android.telephony.satellite.SatelliteModemStateCallback import android.util.Log import android.view.WindowManager import androidx.lifecycle.LifecycleOwner @@ -31,12 +32,19 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers.Default import kotlinx.coroutines.Job import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import java.util.concurrent.ExecutionException import java.util.concurrent.TimeoutException import kotlin.coroutines.resume +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOn /** A util for Satellite dialog */ object SatelliteDialogUtils { @@ -70,7 +78,7 @@ object SatelliteDialogUtils { coroutineScope.launch { var isSatelliteModeOn = false try { - isSatelliteModeOn = requestIsEnabled(context) + isSatelliteModeOn = requestIsSessionStarted(context) } catch (e: InterruptedException) { Log.w(TAG, "Error to get satellite status : $e") } catch (e: ExecutionException) { @@ -134,6 +142,70 @@ object SatelliteDialogUtils { } } + private suspend fun requestIsSessionStarted( + context: Context + ): Boolean = withContext(Default) { + getIsSessionStartedFlow(context).conflate().first() + } + + /** + * Provides a Flow that emits the session state of the satellite modem. Updates are triggered + * when the modem state changes. + * + * @param defaultDispatcher The CoroutineDispatcher to use (Defaults to `Dispatchers.Default`). + * @return A Flow emitting `true` when the session is started and `false` otherwise. + */ + private fun getIsSessionStartedFlow( + context: Context + ): Flow<Boolean> { + val satelliteManager: SatelliteManager? = + context.getSystemService(SatelliteManager::class.java) + if (satelliteManager == null) { + Log.w(TAG, "SatelliteManager is null") + return flowOf(false) + } + + return callbackFlow { + val callback = SatelliteModemStateCallback { state -> + val isSessionStarted = isSatelliteSessionStarted(state) + Log.i(TAG, "Satellite modem state changed: state=$state" + + ", isSessionStarted=$isSessionStarted") + trySend(isSessionStarted) + } + + val registerResult = satelliteManager.registerForModemStateChanged( + Default.asExecutor(), + callback + ) + + if (registerResult != SatelliteManager.SATELLITE_RESULT_SUCCESS) { + // If the registration failed (e.g., device doesn't support satellite), + // SatelliteManager will not emit the current state by callback. + // We send `false` value by ourself to make sure the flow has initial value. + Log.w(TAG, "Failed to register for satellite modem state change: $registerResult") + trySend(false) + } + + awaitClose { satelliteManager.unregisterForModemStateChanged(callback) } + }.flowOn(Default) + } + + + /** + * Check if the modem is in a satellite session. + * + * @param state The SatelliteModemState provided by the SatelliteManager. + * @return `true` if the modem is in a satellite session, `false` otherwise. + */ + fun isSatelliteSessionStarted(@SatelliteManager.SatelliteModemState state: Int): Boolean { + return when (state) { + SatelliteManager.SATELLITE_MODEM_STATE_OFF, + SatelliteManager.SATELLITE_MODEM_STATE_UNAVAILABLE, + SatelliteManager.SATELLITE_MODEM_STATE_UNKNOWN -> false + else -> true + } + } + const val TAG = "SatelliteDialogUtils" const val EXTRA_TYPE_OF_SATELLITE_WARNING_DIALOG: String = diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/satellite/SatelliteDialogUtilsTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/satellite/SatelliteDialogUtilsTest.kt index aeda1ed66d16..31d71308e8d4 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/satellite/SatelliteDialogUtilsTest.kt +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/satellite/SatelliteDialogUtilsTest.kt @@ -17,11 +17,12 @@ package com.android.settingslib.satellite import android.content.Context -import android.content.Intent -import android.os.OutcomeReceiver import android.platform.test.annotations.RequiresFlagsEnabled import android.telephony.satellite.SatelliteManager -import android.telephony.satellite.SatelliteManager.SatelliteException +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_ENABLING_SATELLITE +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_OFF +import android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_MODEM_ERROR +import android.telephony.satellite.SatelliteModemStateCallback import android.util.AndroidRuntimeException import androidx.test.core.app.ApplicationProvider import com.android.internal.telephony.flags.Flags @@ -67,26 +68,21 @@ class SatelliteDialogUtilsTest { @Test @RequiresFlagsEnabled(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) fun mayStartSatelliteWarningDialog_satelliteIsOn_showWarningDialog() = runBlocking { - `when`( - satelliteManager.requestIsEnabled( - any(), any<OutcomeReceiver<Boolean, SatelliteManager.SatelliteException>>() - ) - ) + `when`(satelliteManager.registerForModemStateChanged(any(), any())) .thenAnswer { invocation -> - val receiver = invocation - .getArgument< - OutcomeReceiver<Boolean, SatelliteManager.SatelliteException>>( + val callback = invocation + .getArgument<SatelliteModemStateCallback>( 1 ) - receiver.onResult(true) + callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_ENABLING_SATELLITE) null } try { SatelliteDialogUtils.mayStartSatelliteWarningDialog( context, coroutineScope, TYPE_IS_WIFI, allowClick = { - assertTrue(it) - }) + assertTrue(it) + }) } catch (e: AndroidRuntimeException) { // Catch exception of starting activity . } @@ -95,68 +91,49 @@ class SatelliteDialogUtilsTest { @Test @RequiresFlagsEnabled(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) fun mayStartSatelliteWarningDialog_satelliteIsOff_notShowWarningDialog() = runBlocking { - `when`( - satelliteManager.requestIsEnabled( - any(), any<OutcomeReceiver<Boolean, SatelliteManager.SatelliteException>>() - ) - ) + `when`(satelliteManager.registerForModemStateChanged(any(), any())) .thenAnswer { invocation -> - val receiver = invocation - .getArgument< - OutcomeReceiver<Boolean, SatelliteManager.SatelliteException>>( + val callback = invocation + .getArgument<SatelliteModemStateCallback>( 1 ) - receiver.onResult(false) + callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_OFF) null } SatelliteDialogUtils.mayStartSatelliteWarningDialog( - context, coroutineScope, TYPE_IS_WIFI, allowClick = { - assertFalse(it) - }) + context, coroutineScope, TYPE_IS_WIFI, allowClick = { + assertFalse(it) + }) - verify(context, Times(0)).startActivity(any<Intent>()) + verify(context, Times(0)).startActivity(any()) } @Test @RequiresFlagsEnabled(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) fun mayStartSatelliteWarningDialog_noSatelliteManager_notShowWarningDialog() = runBlocking { - `when`(context.getSystemService(SatelliteManager::class.java)) - .thenReturn(null) + `when`(context.getSystemService(SatelliteManager::class.java)).thenReturn(null) SatelliteDialogUtils.mayStartSatelliteWarningDialog( - context, coroutineScope, TYPE_IS_WIFI, allowClick = { - assertFalse(it) - }) + context, coroutineScope, TYPE_IS_WIFI, allowClick = { + assertFalse(it) + }) - verify(context, Times(0)).startActivity(any<Intent>()) + verify(context, Times(0)).startActivity(any()) } @Test @RequiresFlagsEnabled(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) fun mayStartSatelliteWarningDialog_satelliteErrorResult_notShowWarningDialog() = runBlocking { - `when`( - satelliteManager.requestIsEnabled( - any(), any<OutcomeReceiver<Boolean, SatelliteManager.SatelliteException>>() - ) - ) - .thenAnswer { invocation -> - val receiver = invocation - .getArgument< - OutcomeReceiver<Boolean, SatelliteManager.SatelliteException>>( - 1 - ) - receiver.onError(SatelliteException(SatelliteManager.SATELLITE_RESULT_ERROR)) - null - } - + `when`(satelliteManager.registerForModemStateChanged(any(), any())) + .thenReturn(SATELLITE_RESULT_MODEM_ERROR) SatelliteDialogUtils.mayStartSatelliteWarningDialog( - context, coroutineScope, TYPE_IS_WIFI, allowClick = { - assertFalse(it) - }) + context, coroutineScope, TYPE_IS_WIFI, allowClick = { + assertFalse(it) + }) - verify(context, Times(0)).startActivity(any<Intent>()) + verify(context, Times(0)).startActivity(any()) } } 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 3be5231a8017..368085f3018a 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 @@ -32,6 +32,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.Back +import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.ElementMatcher @@ -70,7 +71,7 @@ object Communal { } object AllElements : ElementMatcher { - override fun matches(key: ElementKey, scene: SceneKey) = true + override fun matches(key: ElementKey, content: ContentKey) = true } private object TransitionDuration { 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 859c0366a52f..df068c4eb4ef 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 @@ -92,7 +92,7 @@ constructor( fun SceneScope.Notifications(burnInParams: BurnInParameters?, modifier: Modifier = Modifier) { val areNotificationsVisible by lockscreenContentViewModel - .areNotificationsVisible(sceneKey) + .areNotificationsVisible(contentKey) .collectAsStateWithLifecycle(initialValue = false) if (!areNotificationsVisible) { return diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt new file mode 100644 index 000000000000..4b3a39b367c9 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt @@ -0,0 +1,100 @@ +/* + * 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.notifications.ui.composable + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.layout.offset +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.unit.IntOffset +import com.android.compose.nestedscroll.PriorityNestedScrollConnection +import kotlin.math.roundToInt +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@Composable +fun Modifier.stackVerticalOverscroll( + coroutineScope: CoroutineScope, + canScrollForward: () -> Boolean +): Modifier { + val overscrollOffset = remember { Animatable(0f) } + val stackNestedScrollConnection = remember { + NotificationStackNestedScrollConnection( + stackOffset = { overscrollOffset.value }, + canScrollForward = canScrollForward, + onScroll = { offsetAvailable -> + coroutineScope.launch { + overscrollOffset.snapTo(overscrollOffset.value + offsetAvailable * 0.3f) + } + }, + onStop = { velocityAvailable -> + coroutineScope.launch { + overscrollOffset.animateTo( + targetValue = 0f, + initialVelocity = velocityAvailable, + animationSpec = tween() + ) + } + } + ) + } + + return this.then( + Modifier.nestedScroll(stackNestedScrollConnection).offset { + IntOffset(x = 0, y = overscrollOffset.value.roundToInt()) + } + ) +} + +fun NotificationStackNestedScrollConnection( + stackOffset: () -> Float, + canScrollForward: () -> Boolean, + onStart: (Float) -> Unit = {}, + onScroll: (Float) -> Unit, + onStop: (Float) -> Unit = {}, +): PriorityNestedScrollConnection { + return PriorityNestedScrollConnection( + orientation = Orientation.Vertical, + canStartPreScroll = { _, _ -> false }, + canStartPostScroll = { offsetAvailable, offsetBeforeStart -> + offsetAvailable < 0f && offsetBeforeStart < 0f && !canScrollForward() + }, + canStartPostFling = { velocityAvailable -> velocityAvailable < 0f && !canScrollForward() }, + canContinueScroll = { source -> + if (source == NestedScrollSource.SideEffect) { + stackOffset() > STACK_OVERSCROLL_FLING_MIN_OFFSET + } else { + true + } + }, + canScrollOnFling = true, + onStart = { offsetAvailable -> onStart(offsetAvailable) }, + onScroll = { offsetAvailable -> + onScroll(offsetAvailable) + offsetAvailable + }, + onStop = { velocityAvailable -> + onStop(velocityAvailable) + velocityAvailable + }, + ) +} 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 76a7a10a95f5..2eb7b3f89af5 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 @@ -474,6 +474,7 @@ fun SceneScope.NotificationScrollingStack( .thenIf(shadeMode == ShadeMode.Single) { Modifier.nestedScroll(scrimNestedScrollConnection) } + .stackVerticalOverscroll(coroutineScope) { scrollState.canScrollForward } .verticalScroll(scrollState) .padding(top = topPadding) .fillMaxWidth() @@ -671,3 +672,4 @@ private val DEBUG_HUN_COLOR = Color(0f, 0f, 1f, 0.2f) private val DEBUG_BOX_COLOR = Color(0f, 1f, 0f, 0.2f) private const val HUN_SNOOZE_POSITIONAL_THRESHOLD_FRACTION = 0.25f private const val HUN_SNOOZE_VELOCITY_THRESHOLD = -70f +internal const val STACK_OVERSCROLL_FLING_MIN_OFFSET = -100f diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt index 114dcf4fbc7e..afbc8e71c940 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt @@ -67,15 +67,15 @@ interface AnimatedState<T> : State<T> { /** * Animate a scene Int value. * - * @see SceneScope.animateSceneValueAsState + * @see ContentScope.animateContentValueAsState */ @Composable -fun SceneScope.animateSceneIntAsState( +fun ContentScope.animateContentIntAsState( value: Int, key: ValueKey, canOverflow: Boolean = true, ): AnimatedState<Int> { - return animateSceneValueAsState(value, key, SharedIntType, canOverflow) + return animateContentValueAsState(value, key, SharedIntType, canOverflow) } /** @@ -107,17 +107,28 @@ private object SharedIntType : SharedValueType<Int, Int> { /** * Animate a scene Float value. * - * @see SceneScope.animateSceneValueAsState + * @see ContentScope.animateContentValueAsState */ @Composable -fun SceneScope.animateSceneFloatAsState( +fun ContentScope.animateContentFloatAsState( value: Float, key: ValueKey, canOverflow: Boolean = true, ): AnimatedState<Float> { - return animateSceneValueAsState(value, key, SharedFloatType, canOverflow) + return animateContentValueAsState(value, key, SharedFloatType, canOverflow) } +@Deprecated( + "Use animateSceneFloatAsState() instead", + replaceWith = ReplaceWith("animateContentFloatAsState(value, key, canOverflow)") +) +@Composable +fun ContentScope.animateSceneFloatAsState( + value: Float, + key: ValueKey, + canOverflow: Boolean = true, +) = animateContentFloatAsState(value, key, canOverflow) + /** * Animate a shared element Float value. * @@ -147,17 +158,28 @@ private object SharedFloatType : SharedValueType<Float, Float> { /** * Animate a scene Dp value. * - * @see SceneScope.animateSceneValueAsState + * @see ContentScope.animateContentValueAsState */ @Composable -fun SceneScope.animateSceneDpAsState( +fun ContentScope.animateContentDpAsState( value: Dp, key: ValueKey, canOverflow: Boolean = true, ): AnimatedState<Dp> { - return animateSceneValueAsState(value, key, SharedDpType, canOverflow) + return animateContentValueAsState(value, key, SharedDpType, canOverflow) } +@Deprecated( + "Use animateSceneDpAsState() instead", + replaceWith = ReplaceWith("animateContentDpAsState(value, key, canOverflow)") +) +@Composable +fun ContentScope.animateSceneDpAsState( + value: Dp, + key: ValueKey, + canOverflow: Boolean = true, +) = animateContentDpAsState(value, key, canOverflow) + /** * Animate a shared element Dp value. * @@ -188,14 +210,14 @@ private object SharedDpType : SharedValueType<Dp, Dp> { /** * Animate a scene Color value. * - * @see SceneScope.animateSceneValueAsState + * @see ContentScope.animateContentValueAsState */ @Composable -fun SceneScope.animateSceneColorAsState( +fun ContentScope.animateContentColorAsState( value: Color, key: ValueKey, ): AnimatedState<Color> { - return animateSceneValueAsState(value, key, SharedColorType, canOverflow = false) + return animateContentValueAsState(value, key, SharedColorType, canOverflow = false) } /** @@ -261,24 +283,24 @@ private class ColorDelta( @Composable internal fun <T> animateSharedValueAsState( layoutImpl: SceneTransitionLayoutImpl, - scene: SceneKey, + content: ContentKey, element: ElementKey?, key: ValueKey, value: T, type: SharedValueType<T, *>, canOverflow: Boolean, ): AnimatedState<T> { - DisposableEffect(layoutImpl, scene, element, key) { - // Create the associated maps that hold the current value for each (element, scene) pair. + DisposableEffect(layoutImpl, content, element, key) { + // Create the associated maps that hold the current value for each (element, content) pair. val valueMap = layoutImpl.sharedValues.getOrPut(key) { mutableMapOf() } val sharedValue = valueMap.getOrPut(element) { SharedValue(type) } as SharedValue<T, *> val targetValues = sharedValue.targetValues - targetValues[scene] = value + targetValues[content] = value onDispose { // Remove the value associated to the current scene, and eventually remove the maps if // they are empty. - targetValues.remove(scene) + targetValues.remove(content) if (targetValues.isEmpty() && valueMap[element] === sharedValue) { valueMap.remove(element) @@ -297,11 +319,11 @@ internal fun <T> animateSharedValueAsState( error("value is equal to $value, which is the undefined value for this type.") } - sharedValue<T, Any>(layoutImpl, key, element).targetValues[scene] = value + sharedValue<T, Any>(layoutImpl, key, element).targetValues[content] = value } - return remember(layoutImpl, scene, element, canOverflow) { - AnimatedStateImpl<T, Any>(layoutImpl, scene, element, key, canOverflow) + return remember(layoutImpl, content, element, canOverflow) { + AnimatedStateImpl<T, Any>(layoutImpl, content, element, key, canOverflow) } } @@ -322,8 +344,8 @@ private fun valueReadTooEarlyMessage(key: ValueKey) = internal class SharedValue<T, Delta>( val type: SharedValueType<T, Delta>, ) { - /** The target value of this shared value for each scene. */ - val targetValues = SnapshotStateMap<SceneKey, T>() + /** The target value of this shared value for each content. */ + val targetValues = SnapshotStateMap<ContentKey, T>() /** The last value of this shared value. */ var lastValue: T = type.unspecifiedValue @@ -340,7 +362,7 @@ internal class SharedValue<T, Delta>( private class AnimatedStateImpl<T, Delta>( private val layoutImpl: SceneTransitionLayoutImpl, - private val scene: SceneKey, + private val content: ContentKey, private val element: ElementKey?, private val key: ValueKey, private val canOverflow: Boolean, @@ -356,14 +378,14 @@ private class AnimatedStateImpl<T, Delta>( // TODO(b/311600838): Remove this. We should not have to fallback to the current // scene value, but we have to because code of removed nodes can still run if they // are placed with a graphics layer. - ?: sharedValue[scene] + ?: sharedValue[content] ?: error(valueReadTooEarlyMessage(key)) val interruptedValue = computeInterruptedValue(sharedValue, transition, value) sharedValue.lastValue = interruptedValue return interruptedValue } - private operator fun SharedValue<T, *>.get(scene: SceneKey): T? = targetValues[scene] + private operator fun SharedValue<T, *>.get(content: ContentKey): T? = targetValues[content] private fun valueOrNull( sharedValue: SharedValue<T, *>, @@ -401,7 +423,7 @@ private class AnimatedStateImpl<T, Delta>( val targetValues = sharedValue.targetValues val transition = if (element != null) { - layoutImpl.elements[element]?.sceneStates?.let { sceneStates -> + layoutImpl.elements[element]?.stateByContent?.let { sceneStates -> layoutImpl.state.currentTransitions.fastLastOrNull { transition -> transition.fromScene in sceneStates || transition.toScene in sceneStates } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index fb13b57176c6..67d1b59d9522 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.round import com.android.compose.animation.scene.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified +import com.android.compose.animation.scene.content.Scene import com.android.compose.nestedscroll.PriorityNestedScrollConnection import kotlin.math.absoluteValue import kotlinx.coroutines.CoroutineScope diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt index 3ad07d0b7b4b..0b5e58faf1db 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt @@ -48,6 +48,7 @@ import androidx.compose.ui.unit.round import androidx.compose.ui.util.fastCoerceIn import androidx.compose.ui.util.fastLastOrNull import androidx.compose.ui.util.lerp +import com.android.compose.animation.scene.content.Content import com.android.compose.animation.scene.transformation.PropertyTransformation import com.android.compose.animation.scene.transformation.SharedElementTransformation import com.android.compose.ui.util.lerp @@ -57,30 +58,30 @@ import kotlinx.coroutines.launch /** An element on screen, that can be composed in one or more scenes. */ @Stable internal class Element(val key: ElementKey) { - /** The mapping between a scene and the state this element has in that scene, if any. */ + /** The mapping between a content and the state this element has in that content, if any. */ // TODO(b/316901148): Make this a normal map instead once we can make sure that new transitions // are first seen by composition then layout/drawing code. See b/316901148#comment2 for details. - val sceneStates = SnapshotStateMap<SceneKey, SceneState>() + val stateByContent = SnapshotStateMap<ContentKey, State>() /** * The last transition that was used when computing the state (size, position and alpha) of this - * element in any scene, or `null` if it was last laid out when idle. + * element in any content, or `null` if it was last laid out when idle. */ var lastTransition: TransitionState.Transition? = null - /** Whether this element was ever drawn in a scene. */ - var wasDrawnInAnyScene = false + /** Whether this element was ever drawn in a content. */ + var wasDrawnInAnyContent = false override fun toString(): String { return "Element(key=$key)" } - /** The last and target state of this element in a given scene. */ + /** The last and target state of this element in a given content. */ @Stable - class SceneState(val scene: SceneKey) { + class State(val content: ContentKey) { /** - * The *target* state of this element in this scene, i.e. the state of this element when we - * are idle on this scene. + * The *target* state of this element in this content, i.e. the state of this element when + * we are idle on this content. */ var targetSize by mutableStateOf(SizeUnspecified) var targetOffset by mutableStateOf(Offset.Unspecified) @@ -91,7 +92,9 @@ internal class Element(val key: ElementKey) { var lastScale = Scale.Unspecified var lastAlpha = AlphaUnspecified - /** The state of this element in this scene right before the last interruption (if any). */ + /** + * The state of this element in this content right before the last interruption (if any). + */ var offsetBeforeInterruption = Offset.Unspecified var sizeBeforeInterruption = SizeUnspecified var scaleBeforeInterruption = Scale.Unspecified @@ -109,7 +112,7 @@ internal class Element(val key: ElementKey) { var alphaInterruptionDelta = 0f /** - * The attached [ElementNode] a Modifier.element() for a given element and scene. During + * The attached [ElementNode] a Modifier.element() for a given element and content. During * composition, this set could have 0 to 2 elements. After composition and after all * modifier nodes have been attached/detached, this set should contain exactly 1 element. */ @@ -130,19 +133,19 @@ data class Scale(val scaleX: Float, val scaleY: Float, val pivot: Offset = Offse } } -/** The implementation of [SceneScope.element]. */ +/** The implementation of [ContentScope.element]. */ @Stable internal fun Modifier.element( layoutImpl: SceneTransitionLayoutImpl, - scene: Scene, + content: Content, key: ElementKey, ): Modifier { // Make sure that we read the current transitions during composition and not during // layout/drawing. // TODO(b/341072461): Revert this and read the current transitions in ElementNode directly once - // we can ensure that SceneTransitionLayoutImpl will compose new scenes first. + // we can ensure that SceneTransitionLayoutImpl will compose new contents first. val currentTransitions = layoutImpl.state.currentTransitions - return then(ElementModifier(layoutImpl, currentTransitions, scene, key)).testTag(key.testTag) + return then(ElementModifier(layoutImpl, currentTransitions, content, key)).testTag(key.testTag) } /** @@ -152,92 +155,92 @@ internal fun Modifier.element( private data class ElementModifier( private val layoutImpl: SceneTransitionLayoutImpl, private val currentTransitions: List<TransitionState.Transition>, - private val scene: Scene, + private val content: Content, private val key: ElementKey, ) : ModifierNodeElement<ElementNode>() { - override fun create(): ElementNode = ElementNode(layoutImpl, currentTransitions, scene, key) + override fun create(): ElementNode = ElementNode(layoutImpl, currentTransitions, content, key) override fun update(node: ElementNode) { - node.update(layoutImpl, currentTransitions, scene, key) + node.update(layoutImpl, currentTransitions, content, key) } } internal class ElementNode( private var layoutImpl: SceneTransitionLayoutImpl, private var currentTransitions: List<TransitionState.Transition>, - private var scene: Scene, + private var content: Content, private var key: ElementKey, ) : Modifier.Node(), DrawModifierNode, ApproachLayoutModifierNode, TraversableNode { private var _element: Element? = null private val element: Element get() = _element!! - private var _sceneState: Element.SceneState? = null - private val sceneState: Element.SceneState - get() = _sceneState!! + private var _stateInContent: Element.State? = null + private val stateInContent: Element.State + get() = _stateInContent!! override val traverseKey: Any = ElementTraverseKey override fun onAttach() { super.onAttach() - updateElementAndSceneValues() - addNodeToSceneState() + updateElementAndContentValues() + addNodeToContentState() } - private fun updateElementAndSceneValues() { + private fun updateElementAndContentValues() { val element = layoutImpl.elements[key] ?: Element(key).also { layoutImpl.elements[key] = it } _element = element - _sceneState = - element.sceneStates[scene.key] - ?: Element.SceneState(scene.key).also { element.sceneStates[scene.key] = it } + _stateInContent = + element.stateByContent[content.key] + ?: Element.State(content.key).also { element.stateByContent[content.key] = it } } - private fun addNodeToSceneState() { - sceneState.nodes.add(this) + private fun addNodeToContentState() { + stateInContent.nodes.add(this) coroutineScope.launch { // At this point all [CodeLocationNode] have been attached or detached, which means that - // [sceneState.codeLocations] should have exactly 1 element, otherwise this means that - // this element was composed multiple times in the same scene. - val nCodeLocations = sceneState.nodes.size - if (nCodeLocations != 1 || !sceneState.nodes.contains(this@ElementNode)) { - error("$key was composed $nCodeLocations times in ${sceneState.scene}") + // [elementState.codeLocations] should have exactly 1 element, otherwise this means that + // this element was composed multiple times in the same content. + val nCodeLocations = stateInContent.nodes.size + if (nCodeLocations != 1 || !stateInContent.nodes.contains(this@ElementNode)) { + error("$key was composed $nCodeLocations times in ${stateInContent.content}") } } } override fun onDetach() { super.onDetach() - removeNodeFromSceneState() - maybePruneMaps(layoutImpl, element, sceneState) + removeNodeFromContentState() + maybePruneMaps(layoutImpl, element, stateInContent) _element = null - _sceneState = null + _stateInContent = null } - private fun removeNodeFromSceneState() { - sceneState.nodes.remove(this) + private fun removeNodeFromContentState() { + stateInContent.nodes.remove(this) } fun update( layoutImpl: SceneTransitionLayoutImpl, currentTransitions: List<TransitionState.Transition>, - scene: Scene, + content: Content, key: ElementKey, ) { - check(layoutImpl == this.layoutImpl && scene == this.scene) + check(layoutImpl == this.layoutImpl && content == this.content) this.currentTransitions = currentTransitions - removeNodeFromSceneState() + removeNodeFromContentState() val prevElement = this.element - val prevSceneState = this.sceneState + val prevElementState = this.stateInContent this.key = key - updateElementAndSceneValues() + updateElementAndContentValues() - addNodeToSceneState() - maybePruneMaps(layoutImpl, prevElement, prevSceneState) + addNodeToContentState() + maybePruneMaps(layoutImpl, prevElement, prevElementState) } override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean { @@ -262,15 +265,15 @@ internal class ElementNode( check(isLookingAhead) return measurable.measure(constraints).run { - // Update the size this element has in this scene when idle. - sceneState.targetSize = size() + // Update the size this element has in this content when idle. + stateInContent.targetSize = size() layout(width, height) { // Update the offset (relative to the SceneTransitionLayout) this element has in - // this scene when idle. + // this content when idle. coordinates?.let { coords -> with(layoutImpl.lookaheadScope) { - sceneState.targetOffset = + stateInContent.targetOffset = lookaheadScopeCoordinates.localLookaheadPositionOf(coords) } } @@ -287,22 +290,22 @@ internal class ElementNode( val transition = elementTransition(layoutImpl, element, transitions) // If this element is not supposed to be laid out now, either because it is not part of any - // ongoing transition or the other scene of its transition is overscrolling, then lay out + // ongoing transition or the other content of its transition is overscrolling, then lay out // the element normally and don't place it. val overscrollScene = transition?.currentOverscrollSpec?.scene - val isOtherSceneOverscrolling = overscrollScene != null && overscrollScene != scene.key + val isOtherSceneOverscrolling = overscrollScene != null && overscrollScene != content.key val isNotPartOfAnyOngoingTransitions = transitions.isNotEmpty() && transition == null if (isNotPartOfAnyOngoingTransitions || isOtherSceneOverscrolling) { recursivelyClearPlacementValues() - sceneState.lastSize = Element.SizeUnspecified + stateInContent.lastSize = Element.SizeUnspecified val placeable = measurable.measure(constraints) return layout(placeable.width, placeable.height) { /* Do not place */ } } val placeable = - measure(layoutImpl, element, transition, sceneState, measurable, constraints) - sceneState.lastSize = placeable.size() + measure(layoutImpl, element, transition, stateInContent, measurable, constraints) + stateInContent.lastSize = placeable.size() return layout(placeable.width, placeable.height) { place(transition, placeable) } } @@ -312,12 +315,12 @@ internal class ElementNode( ) { with(layoutImpl.lookaheadScope) { // Update the offset (relative to the SceneTransitionLayout) this element has in this - // scene when idle. + // content when idle. val coords = coordinates ?: error("Element ${element.key} does not have any coordinates") - // No need to place the element in this scene if we don't want to draw it anyways. - if (!shouldPlaceElement(layoutImpl, scene.key, element, transition)) { + // No need to place the element in this content if we don't want to draw it anyways. + if (!shouldPlaceElement(layoutImpl, content.key, element, transition)) { recursivelyClearPlacementValues() return } @@ -326,10 +329,10 @@ internal class ElementNode( val targetOffset = computeValue( layoutImpl, - sceneState, + stateInContent, element, transition, - sceneValue = { it.targetOffset }, + contentValue = { it.targetOffset }, transformation = { it.offset }, currentValue = { currentOffset }, isSpecified = { it != Offset.Unspecified }, @@ -343,17 +346,17 @@ internal class ElementNode( value = targetOffset, unspecifiedValue = Offset.Unspecified, zeroValue = Offset.Zero, - getValueBeforeInterruption = { sceneState.offsetBeforeInterruption }, - setValueBeforeInterruption = { sceneState.offsetBeforeInterruption = it }, - getInterruptionDelta = { sceneState.offsetInterruptionDelta }, + getValueBeforeInterruption = { stateInContent.offsetBeforeInterruption }, + setValueBeforeInterruption = { stateInContent.offsetBeforeInterruption = it }, + getInterruptionDelta = { stateInContent.offsetInterruptionDelta }, setInterruptionDelta = { delta -> setPlacementInterruptionDelta( element = element, - sceneState = sceneState, + stateInContent = stateInContent, transition = transition, delta = delta, - setter = { sceneState, delta -> - sceneState.offsetInterruptionDelta = delta + setter = { stateInContent, delta -> + stateInContent.offsetInterruptionDelta = delta }, ) }, @@ -361,14 +364,15 @@ internal class ElementNode( add = { a, b, bProgress -> a + b * bProgress }, ) - sceneState.lastOffset = interruptedOffset + stateInContent.lastOffset = interruptedOffset val offset = (interruptedOffset - currentOffset).round() if ( - isElementOpaque(scene, element, transition) && - interruptedAlpha(layoutImpl, element, transition, sceneState, alpha = 1f) == 1f + isElementOpaque(content, element, transition) && + interruptedAlpha(layoutImpl, element, transition, stateInContent, alpha = 1f) == + 1f ) { - sceneState.lastAlpha = 1f + stateInContent.lastAlpha = 1f // TODO(b/291071158): Call placeWithLayer() if offset != IntOffset.Zero and size is // not animated once b/305195729 is fixed. Test that drawing is not invalidated in @@ -387,11 +391,11 @@ internal class ElementNode( } val transition = elementTransition(layoutImpl, element, currentTransitions) - if (!shouldPlaceElement(layoutImpl, scene.key, element, transition)) { + if (!shouldPlaceElement(layoutImpl, content.key, element, transition)) { return@placeWithLayer } - alpha = elementAlpha(layoutImpl, element, transition, sceneState) + alpha = elementAlpha(layoutImpl, element, transition, stateInContent) compositingStrategy = CompositingStrategy.ModulateAlpha } } @@ -404,24 +408,24 @@ internal class ElementNode( * for the descendants for which approachMeasure() won't be called. */ private fun recursivelyClearPlacementValues() { - fun Element.SceneState.clearLastPlacementValues() { + fun Element.State.clearLastPlacementValues() { lastOffset = Offset.Unspecified lastScale = Scale.Unspecified lastAlpha = Element.AlphaUnspecified } - sceneState.clearLastPlacementValues() + stateInContent.clearLastPlacementValues() traverseDescendants(ElementTraverseKey) { node -> - (node as ElementNode)._sceneState?.clearLastPlacementValues() + (node as ElementNode)._stateInContent?.clearLastPlacementValues() TraversableNode.Companion.TraverseDescendantsAction.ContinueTraversal } } override fun ContentDrawScope.draw() { - element.wasDrawnInAnyScene = true + element.wasDrawnInAnyContent = true val transition = elementTransition(layoutImpl, element, currentTransitions) - val drawScale = getDrawScale(layoutImpl, element, transition, sceneState) + val drawScale = getDrawScale(layoutImpl, element, transition, stateInContent) if (drawScale == Scale.Default) { drawContent() } else { @@ -441,16 +445,21 @@ internal class ElementNode( private fun maybePruneMaps( layoutImpl: SceneTransitionLayoutImpl, element: Element, - sceneState: Element.SceneState, + stateInContent: Element.State, ) { - // If element is not composed from this scene anymore, remove the scene values. This + // If element is not composed in this content anymore, remove the content values. This // works because [onAttach] is called before [onDetach], so if an element is moved from // the UI tree we will first add the new code location then remove the old one. - if (sceneState.nodes.isEmpty() && element.sceneStates[sceneState.scene] == sceneState) { - element.sceneStates.remove(sceneState.scene) + if ( + stateInContent.nodes.isEmpty() && + element.stateByContent[stateInContent.content] == stateInContent + ) { + element.stateByContent.remove(stateInContent.content) - // If the element is not composed in any scene, remove it from the elements map. - if (element.sceneStates.isEmpty() && layoutImpl.elements[element.key] == element) { + // If the element is not composed in any content, remove it from the elements map. + if ( + element.stateByContent.isEmpty() && layoutImpl.elements[element.key] == element + ) { layoutImpl.elements.remove(element.key) } } @@ -460,7 +469,7 @@ internal class ElementNode( /** * The transition that we should consider for [element]. This is the last transition where one of - * its scenes contains the element. + * its contents contains the element. */ private fun elementTransition( layoutImpl: SceneTransitionLayoutImpl, @@ -469,7 +478,8 @@ private fun elementTransition( ): TransitionState.Transition? { val transition = transitions.fastLastOrNull { transition -> - transition.fromScene in element.sceneStates || transition.toScene in element.sceneStates + transition.fromScene in element.stateByContent || + transition.toScene in element.stateByContent } val previousTransition = element.lastTransition @@ -480,7 +490,7 @@ private fun elementTransition( prepareInterruption(layoutImpl, element, transition, previousTransition) } else if (transition == null && previousTransition != null) { // The transition was just finished. - element.sceneStates.values.forEach { + element.stateByContent.values.forEach { it.clearValuesBeforeInterruption() it.clearInterruptionDeltas() } @@ -499,32 +509,32 @@ private fun prepareInterruption( return } - val sceneStates = element.sceneStates - fun updatedSceneState(key: SceneKey): Element.SceneState? { - return sceneStates[key]?.also { it.selfUpdateValuesBeforeInterruption() } + val stateByContent = element.stateByContent + fun updateStateInContent(key: ContentKey): Element.State? { + return stateByContent[key]?.also { it.selfUpdateValuesBeforeInterruption() } } - val previousFromState = updatedSceneState(previousTransition.fromScene) - val previousToState = updatedSceneState(previousTransition.toScene) - val fromState = updatedSceneState(transition.fromScene) - val toState = updatedSceneState(transition.toScene) + val previousFromState = updateStateInContent(previousTransition.fromScene) + val previousToState = updateStateInContent(previousTransition.toScene) + val fromState = updateStateInContent(transition.fromScene) + val toState = updateStateInContent(transition.toScene) reconcileStates(element, previousTransition) reconcileStates(element, transition) - // Remove the interruption values to all scenes but the scene(s) where the element will be + // Remove the interruption values to all contents but the content(s) where the element will be // placed, to make sure that interruption deltas are computed only right after this interruption // is prepared. - fun cleanInterruptionValues(sceneState: Element.SceneState) { - sceneState.sizeInterruptionDelta = IntSize.Zero - sceneState.offsetInterruptionDelta = Offset.Zero - sceneState.alphaInterruptionDelta = 0f - sceneState.scaleInterruptionDelta = Scale.Zero - - if (!shouldPlaceElement(layoutImpl, sceneState.scene, element, transition)) { - sceneState.offsetBeforeInterruption = Offset.Unspecified - sceneState.alphaBeforeInterruption = Element.AlphaUnspecified - sceneState.scaleBeforeInterruption = Scale.Unspecified + fun cleanInterruptionValues(stateInContent: Element.State) { + stateInContent.sizeInterruptionDelta = IntSize.Zero + stateInContent.offsetInterruptionDelta = Offset.Zero + stateInContent.alphaInterruptionDelta = 0f + stateInContent.scaleInterruptionDelta = Scale.Zero + + if (!shouldPlaceElement(layoutImpl, stateInContent.content, element, transition)) { + stateInContent.offsetBeforeInterruption = Offset.Unspecified + stateInContent.alphaBeforeInterruption = Element.AlphaUnspecified + stateInContent.scaleBeforeInterruption = Scale.Unspecified } } @@ -542,8 +552,8 @@ private fun reconcileStates( element: Element, transition: TransitionState.Transition, ) { - val fromSceneState = element.sceneStates[transition.fromScene] ?: return - val toSceneState = element.sceneStates[transition.toScene] ?: return + val fromSceneState = element.stateByContent[transition.fromScene] ?: return + val toSceneState = element.stateByContent[transition.toScene] ?: return if (!isSharedElementEnabled(element.key, transition)) { return } @@ -563,7 +573,7 @@ private fun reconcileStates( } } -private fun Element.SceneState.selfUpdateValuesBeforeInterruption() { +private fun Element.State.selfUpdateValuesBeforeInterruption() { sizeBeforeInterruption = lastSize if (lastAlpha > 0f) { @@ -571,7 +581,7 @@ private fun Element.SceneState.selfUpdateValuesBeforeInterruption() { scaleBeforeInterruption = lastScale alphaBeforeInterruption = lastAlpha } else { - // Consider the element as not placed in this scene if it was fully transparent. + // Consider the element as not placed in this content if it was fully transparent. // TODO(b/290930950): Look into using derived state inside place() instead to not even place // the element at all when alpha == 0f. offsetBeforeInterruption = Offset.Unspecified @@ -580,7 +590,7 @@ private fun Element.SceneState.selfUpdateValuesBeforeInterruption() { } } -private fun Element.SceneState.updateValuesBeforeInterruption(lastState: Element.SceneState) { +private fun Element.State.updateValuesBeforeInterruption(lastState: Element.State) { offsetBeforeInterruption = lastState.offsetBeforeInterruption sizeBeforeInterruption = lastState.sizeBeforeInterruption scaleBeforeInterruption = lastState.scaleBeforeInterruption @@ -589,14 +599,14 @@ private fun Element.SceneState.updateValuesBeforeInterruption(lastState: Element clearInterruptionDeltas() } -private fun Element.SceneState.clearInterruptionDeltas() { +private fun Element.State.clearInterruptionDeltas() { offsetInterruptionDelta = Offset.Zero sizeInterruptionDelta = IntSize.Zero scaleInterruptionDelta = Scale.Zero alphaInterruptionDelta = 0f } -private fun Element.SceneState.clearValuesBeforeInterruption() { +private fun Element.State.clearValuesBeforeInterruption() { offsetBeforeInterruption = Offset.Unspecified scaleBeforeInterruption = Scale.Unspecified alphaBeforeInterruption = Element.AlphaUnspecified @@ -655,13 +665,13 @@ private inline fun <T> computeInterruptedValue( */ private inline fun <T> setPlacementInterruptionDelta( element: Element, - sceneState: Element.SceneState, + stateInContent: Element.State, transition: TransitionState.Transition?, delta: T, - setter: (Element.SceneState, T) -> Unit, + setter: (Element.State, T) -> Unit, ) { - // Set the interruption delta on the current scene. - setter(sceneState, delta) + // Set the interruption delta on the current content. + setter(stateInContent, delta) if (transition == null) { return @@ -670,8 +680,9 @@ private inline fun <T> setPlacementInterruptionDelta( // If the element is shared, also set the delta on the other scene so that it is used by that // scene if we start overscrolling it and change the scene where the element is placed. val otherScene = - if (sceneState.scene == transition.fromScene) transition.toScene else transition.fromScene - val otherSceneState = element.sceneStates[otherScene] ?: return + if (stateInContent.content == transition.fromScene) transition.toScene + else transition.fromScene + val otherSceneState = element.stateByContent[otherScene] ?: return if (isSharedElementEnabled(element.key, transition)) { setter(otherSceneState, delta) } @@ -679,7 +690,7 @@ private inline fun <T> setPlacementInterruptionDelta( private fun shouldPlaceElement( layoutImpl: SceneTransitionLayoutImpl, - scene: SceneKey, + content: ContentKey, element: Element, transition: TransitionState.Transition?, ): Boolean { @@ -688,15 +699,16 @@ private fun shouldPlaceElement( return true } - // Don't place the element in this scene if this scene is not part of the current element + // Don't place the element in this content if this content is not part of the current element // transition. - if (scene != transition.fromScene && scene != transition.toScene) { + if (content != transition.fromScene && content != transition.toScene) { return false } // Place the element if it is not shared. if ( - transition.fromScene !in element.sceneStates || transition.toScene !in element.sceneStates + transition.fromScene !in element.stateByContent || + transition.toScene !in element.stateByContent ) { return true } @@ -708,7 +720,7 @@ private fun shouldPlaceElement( return shouldPlaceOrComposeSharedElement( layoutImpl, - scene, + content, element.key, transition, ) @@ -716,14 +728,14 @@ private fun shouldPlaceElement( internal fun shouldPlaceOrComposeSharedElement( layoutImpl: SceneTransitionLayoutImpl, - scene: SceneKey, + content: ContentKey, element: ElementKey, transition: TransitionState.Transition, ): Boolean { // If we are overscrolling, only place/compose the element in the overscrolling scene. val overscrollScene = transition.currentOverscrollSpec?.scene if (overscrollScene != null) { - return scene == overscrollScene + return content == overscrollScene } val scenePicker = element.scenePicker @@ -738,7 +750,7 @@ internal fun shouldPlaceOrComposeSharedElement( toSceneZIndex = layoutImpl.scenes.getValue(toScene).zIndex, ) ?: return false - return pickedScene == scene + return pickedScene == content } private fun isSharedElementEnabled( @@ -775,7 +787,7 @@ internal fun sharedElementTransformation( * placement and we don't want to read the transition progress in that phase. */ private fun isElementOpaque( - scene: Scene, + content: Content, element: Element, transition: TransitionState.Transition?, ): Boolean { @@ -785,8 +797,8 @@ private fun isElementOpaque( val fromScene = transition.fromScene val toScene = transition.toScene - val fromState = element.sceneStates[fromScene] - val toState = element.sceneStates[toScene] + val fromState = element.stateByContent[fromScene] + val toState = element.stateByContent[toScene] if (fromState == null && toState == null) { // TODO(b/311600838): Throw an exception instead once layers of disposed elements are not @@ -799,7 +811,7 @@ private fun isElementOpaque( return true } - return transition.transformationSpec.transformations(element.key, scene.key).alpha == null + return transition.transformationSpec.transformations(element.key, content.key).alpha == null } /** @@ -814,15 +826,15 @@ private fun elementAlpha( layoutImpl: SceneTransitionLayoutImpl, element: Element, transition: TransitionState.Transition?, - sceneState: Element.SceneState, + stateInContent: Element.State, ): Float { val alpha = computeValue( layoutImpl, - sceneState, + stateInContent, element, transition, - sceneValue = { 1f }, + contentValue = { 1f }, transformation = { it.alpha }, currentValue = { 1f }, isSpecified = { true }, @@ -832,12 +844,12 @@ private fun elementAlpha( // If the element is fading during this transition and that it is drawn for the first time, make // sure that it doesn't instantly appear on screen. - if (!element.wasDrawnInAnyScene && alpha > 0f) { - element.sceneStates.forEach { it.value.alphaBeforeInterruption = 0f } + if (!element.wasDrawnInAnyContent && alpha > 0f) { + element.stateByContent.forEach { it.value.alphaBeforeInterruption = 0f } } - val interruptedAlpha = interruptedAlpha(layoutImpl, element, transition, sceneState, alpha) - sceneState.lastAlpha = interruptedAlpha + val interruptedAlpha = interruptedAlpha(layoutImpl, element, transition, stateInContent, alpha) + stateInContent.lastAlpha = interruptedAlpha return interruptedAlpha } @@ -845,7 +857,7 @@ private fun interruptedAlpha( layoutImpl: SceneTransitionLayoutImpl, element: Element, transition: TransitionState.Transition?, - sceneState: Element.SceneState, + stateInContent: Element.State, alpha: Float, ): Float { return computeInterruptedValue( @@ -854,16 +866,16 @@ private fun interruptedAlpha( value = alpha, unspecifiedValue = Element.AlphaUnspecified, zeroValue = 0f, - getValueBeforeInterruption = { sceneState.alphaBeforeInterruption }, - setValueBeforeInterruption = { sceneState.alphaBeforeInterruption = it }, - getInterruptionDelta = { sceneState.alphaInterruptionDelta }, + getValueBeforeInterruption = { stateInContent.alphaBeforeInterruption }, + setValueBeforeInterruption = { stateInContent.alphaBeforeInterruption = it }, + getInterruptionDelta = { stateInContent.alphaInterruptionDelta }, setInterruptionDelta = { delta -> setPlacementInterruptionDelta( element = element, - sceneState = sceneState, + stateInContent = stateInContent, transition = transition, delta = delta, - setter = { sceneState, delta -> sceneState.alphaInterruptionDelta = delta }, + setter = { stateInContent, delta -> stateInContent.alphaInterruptionDelta = delta }, ) }, diff = { a, b -> a - b }, @@ -875,7 +887,7 @@ private fun measure( layoutImpl: SceneTransitionLayoutImpl, element: Element, transition: TransitionState.Transition?, - sceneState: Element.SceneState, + stateInContent: Element.State, measurable: Measurable, constraints: Constraints, ): Placeable { @@ -887,10 +899,10 @@ private fun measure( val targetSize = computeValue( layoutImpl, - sceneState, + stateInContent, element, transition, - sceneValue = { it.targetSize }, + contentValue = { it.targetSize }, transformation = { it.size }, currentValue = { measurable.measure(constraints).also { maybePlaceable = it }.size() }, isSpecified = { it != Element.SizeUnspecified }, @@ -900,8 +912,8 @@ private fun measure( // The measurable was already measured, so we can't take interruptions into account here given // that we are not allowed to measure the same measurable twice. maybePlaceable?.let { placeable -> - sceneState.sizeBeforeInterruption = Element.SizeUnspecified - sceneState.sizeInterruptionDelta = IntSize.Zero + stateInContent.sizeBeforeInterruption = Element.SizeUnspecified + stateInContent.sizeInterruptionDelta = IntSize.Zero return placeable } @@ -912,10 +924,10 @@ private fun measure( value = targetSize, unspecifiedValue = Element.SizeUnspecified, zeroValue = IntSize.Zero, - getValueBeforeInterruption = { sceneState.sizeBeforeInterruption }, - setValueBeforeInterruption = { sceneState.sizeBeforeInterruption = it }, - getInterruptionDelta = { sceneState.sizeInterruptionDelta }, - setInterruptionDelta = { sceneState.sizeInterruptionDelta = it }, + getValueBeforeInterruption = { stateInContent.sizeBeforeInterruption }, + setValueBeforeInterruption = { stateInContent.sizeBeforeInterruption = it }, + getInterruptionDelta = { stateInContent.sizeInterruptionDelta }, + setInterruptionDelta = { stateInContent.sizeInterruptionDelta = it }, diff = { a, b -> IntSize(a.width - b.width, a.height - b.height) }, add = { a, b, bProgress -> IntSize( @@ -939,15 +951,15 @@ private fun ContentDrawScope.getDrawScale( layoutImpl: SceneTransitionLayoutImpl, element: Element, transition: TransitionState.Transition?, - sceneState: Element.SceneState, + stateInContent: Element.State, ): Scale { val scale = computeValue( layoutImpl, - sceneState, + stateInContent, element, transition, - sceneValue = { Scale.Default }, + contentValue = { Scale.Default }, transformation = { it.drawScale }, currentValue = { Scale.Default }, isSpecified = { true }, @@ -965,16 +977,18 @@ private fun ContentDrawScope.getDrawScale( value = scale, unspecifiedValue = Scale.Unspecified, zeroValue = Scale.Zero, - getValueBeforeInterruption = { sceneState.scaleBeforeInterruption }, - setValueBeforeInterruption = { sceneState.scaleBeforeInterruption = it }, - getInterruptionDelta = { sceneState.scaleInterruptionDelta }, + getValueBeforeInterruption = { stateInContent.scaleBeforeInterruption }, + setValueBeforeInterruption = { stateInContent.scaleBeforeInterruption = it }, + getInterruptionDelta = { stateInContent.scaleInterruptionDelta }, setInterruptionDelta = { delta -> setPlacementInterruptionDelta( element = element, - sceneState = sceneState, + stateInContent = stateInContent, transition = transition, delta = delta, - setter = { sceneState, delta -> sceneState.scaleInterruptionDelta = delta }, + setter = { stateInContent, delta -> + stateInContent.scaleInterruptionDelta = delta + }, ) }, diff = { a, b -> @@ -1003,7 +1017,7 @@ private fun ContentDrawScope.getDrawScale( } ) - sceneState.lastScale = interruptedScale + stateInContent.lastScale = interruptedScale return interruptedScale } @@ -1015,11 +1029,11 @@ private fun ContentDrawScope.getDrawScale( * Measurable. * * @param layoutImpl the [SceneTransitionLayoutImpl] associated to [element]. - * @param currentSceneState the scene state of the scene for which we are computing the value. Note - * that during interruptions, this could be the state of a scene that is neither + * @param currentContentState the content state of the content for which we are computing the value. + * Note that during interruptions, this could be the state of a content that is neither * [transition.toScene] nor [transition.fromScene]. * @param element the element being animated. - * @param sceneValue the value being animated. + * @param contentValue the value being animated. * @param transformation the transformation associated to the value being animated. * @param currentValue the value that would be used if it is not transformed. Note that this is * different than [idleValue] even if the value is not transformed directly because it could be @@ -1030,10 +1044,10 @@ private fun ContentDrawScope.getDrawScale( */ private inline fun <T> computeValue( layoutImpl: SceneTransitionLayoutImpl, - currentSceneState: Element.SceneState, + currentContentState: Element.State, element: Element, transition: TransitionState.Transition?, - sceneValue: (Element.SceneState) -> T, + contentValue: (Element.State) -> T, transformation: (ElementTransformations) -> PropertyTransformation<T>?, currentValue: () -> T, isSpecified: (T) -> Boolean, @@ -1050,16 +1064,16 @@ private inline fun <T> computeValue( val fromScene = transition.fromScene val toScene = transition.toScene - val fromState = element.sceneStates[fromScene] - val toState = element.sceneStates[toScene] + val fromState = element.stateByContent[fromScene] + val toState = element.stateByContent[toScene] if (fromState == null && toState == null) { // TODO(b/311600838): Throw an exception instead once layers of disposed elements are not // run anymore. - return sceneValue(currentSceneState) + return contentValue(currentContentState) } - val currentScene = currentSceneState.scene + val currentScene = currentContentState.content if (transition is TransitionState.HasOverscrollProperties) { val overscroll = transition.currentOverscrollSpec if (overscroll?.scene == currentScene) { @@ -1067,7 +1081,7 @@ private inline fun <T> computeValue( overscroll.transformationSpec.transformations(element.key, currentScene) val propertySpec = transformation(elementSpec) ?: return currentValue() val overscrollState = checkNotNull(if (currentScene == toScene) toState else fromState) - val idleValue = sceneValue(overscrollState) + val idleValue = contentValue(overscrollState) val targetValue = propertySpec.transform( layoutImpl, @@ -1102,8 +1116,8 @@ private inline fun <T> computeValue( // elements follow the finger direction. val isSharedElement = fromState != null && toState != null if (isSharedElement && isSharedElementEnabled(element.key, transition)) { - val start = sceneValue(fromState!!) - val end = sceneValue(toState!!) + val start = contentValue(fromState!!) + val end = contentValue(toState!!) // TODO(b/316901148): Remove checks to isSpecified() once the lookahead pass runs for all // nodes before the intermediate layout pass. @@ -1117,7 +1131,7 @@ private inline fun <T> computeValue( // Get the transformed value, i.e. the target value at the beginning (for entering elements) or // end (for leaving elements) of the transition. - val sceneState = + val contentState = checkNotNull( when { isSharedElement && currentScene == fromScene -> fromState @@ -1129,26 +1143,26 @@ private inline fun <T> computeValue( // The scene for which we compute the transformation. Note that this is not necessarily // [currentScene] because [currentScene] could be a different scene than the transition // fromScene or toScene during interruptions. - val scene = sceneState.scene + val content = contentState.content val transformation = - transformation(transition.transformationSpec.transformations(element.key, scene)) + transformation(transition.transformationSpec.transformations(element.key, content)) val previewTransformation = transition.previewTransformationSpec?.let { - transformation(it.transformations(element.key, scene)) + transformation(it.transformations(element.key, content)) } if (previewTransformation != null) { val isInPreviewStage = transition.isInPreviewStage - val idleValue = sceneValue(sceneState) - val isEntering = scene == toScene + val idleValue = contentValue(contentState) + val isEntering = content == toScene val previewTargetValue = previewTransformation.transform( layoutImpl, - scene, + content, element, - sceneState, + contentState, transition, idleValue, ) @@ -1156,9 +1170,9 @@ private inline fun <T> computeValue( val targetValueOrNull = transformation?.transform( layoutImpl, - scene, + content, element, - sceneState, + contentState, transition, idleValue, ) @@ -1226,13 +1240,13 @@ private inline fun <T> computeValue( return currentValue() } - val idleValue = sceneValue(sceneState) + val idleValue = contentValue(contentState) val targetValue = transformation.transform( layoutImpl, - scene, + content, element, - sceneState, + contentState, transition, idleValue, ) @@ -1248,7 +1262,7 @@ private inline fun <T> computeValue( val rangeProgress = transformation.range?.progress(progress) ?: progress // Interpolate between the value at rest and the value before entering/after leaving. - val isEntering = scene == toScene + val isEntering = content == toScene return if (isEntering) { lerp(targetValue, idleValue, rangeProgress) } else { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ElementMatcher.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ElementMatcher.kt index 98dbb67d7c66..ca68c256fd73 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ElementMatcher.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ElementMatcher.kt @@ -18,20 +18,23 @@ package com.android.compose.animation.scene /** An interface to match one or more elements. */ interface ElementMatcher { - /** Whether the element with key [key] in scene [scene] matches this matcher. */ - fun matches(key: ElementKey, scene: SceneKey): Boolean + /** Whether the element with key [key] in scene [content] matches this matcher. */ + fun matches(key: ElementKey, content: ContentKey): Boolean } /** - * Returns an [ElementMatcher] that matches elements in [scene] also matching [this] + * Returns an [ElementMatcher] that matches elements in [content] also matching [this] * [ElementMatcher]. */ -fun ElementMatcher.inScene(scene: SceneKey): ElementMatcher { +fun ElementMatcher.inContent(content: ContentKey): ElementMatcher { val delegate = this - val matcherScene = scene + val matcherScene = content return object : ElementMatcher { - override fun matches(key: ElementKey, scene: SceneKey): Boolean { - return scene == matcherScene && delegate.matches(key, scene) + override fun matches(key: ElementKey, content: ContentKey): Boolean { + return content == matcherScene && delegate.matches(key, content) } } } + +@Deprecated("Use inContent() instead", replaceWith = ReplaceWith("inContent(scene)")) +fun ElementMatcher.inScene(scene: SceneKey) = inContent(scene) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt index 97703992cbf6..a9edf0afc66f 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt @@ -40,15 +40,20 @@ sealed class Key(val debugName: String, val identity: Any) { } } +/** The key for a content (scene or overlay). */ +sealed class ContentKey(debugName: String, identity: Any) : Key(debugName, identity) { + @VisibleForTesting + // TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can + // access internal members. + abstract val testTag: String +} + /** Key for a scene. */ class SceneKey( debugName: String, identity: Any = Object(), -) : Key(debugName, identity) { - @VisibleForTesting - // TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can - // access internal members. - val testTag: String = "scene:$debugName" +) : ContentKey(debugName, identity) { + override val testTag: String = "scene:$debugName" /** The unique [ElementKey] identifying this scene's root element. */ val rootElementKey = ElementKey(debugName, identity) @@ -74,7 +79,7 @@ class ElementKey( // access internal members. val testTag: String = "element:$debugName" - override fun matches(key: ElementKey, scene: SceneKey): Boolean { + override fun matches(key: ElementKey, content: ContentKey): Boolean { return key == this } @@ -86,7 +91,7 @@ class ElementKey( /** Matches any element whose [key identity][ElementKey.identity] matches [predicate]. */ fun withIdentity(predicate: (Any) -> Boolean): ElementMatcher { return object : ElementMatcher { - override fun matches(key: ElementKey, scene: SceneKey): Boolean { + override fun matches(key: ElementKey, content: ContentKey): Boolean { return predicate(key.identity) } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt index 32eadde7bf30..e556f6f4ff05 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt @@ -27,21 +27,22 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.Layout import androidx.compose.ui.unit.IntSize import androidx.compose.ui.util.fastLastOrNull +import com.android.compose.animation.scene.content.Content @Composable internal fun Element( layoutImpl: SceneTransitionLayoutImpl, - scene: Scene, + sceneOrOverlay: Content, key: ElementKey, modifier: Modifier, content: @Composable ElementScope<ElementContentScope>.() -> Unit, ) { - Box(modifier.element(layoutImpl, scene, key)) { - val sceneScope = scene.scope + Box(modifier.element(layoutImpl, sceneOrOverlay, key)) { + val contentScope = sceneOrOverlay.scope val boxScope = this val elementScope = - remember(layoutImpl, key, scene, sceneScope, boxScope) { - ElementScopeImpl(layoutImpl, key, scene, sceneScope, boxScope) + remember(layoutImpl, key, sceneOrOverlay, contentScope, boxScope) { + ElementScopeImpl(layoutImpl, key, sceneOrOverlay, contentScope, boxScope) } content(elementScope) @@ -51,17 +52,17 @@ internal fun Element( @Composable internal fun MovableElement( layoutImpl: SceneTransitionLayoutImpl, - scene: Scene, + sceneOrOverlay: Content, key: ElementKey, modifier: Modifier, content: @Composable ElementScope<MovableElementContentScope>.() -> Unit, ) { - Box(modifier.element(layoutImpl, scene, key)) { - val sceneScope = scene.scope + Box(modifier.element(layoutImpl, sceneOrOverlay, key)) { + val contentScope = sceneOrOverlay.scope val boxScope = this val elementScope = - remember(layoutImpl, key, scene, sceneScope, boxScope) { - MovableElementScopeImpl(layoutImpl, key, scene, sceneScope, boxScope) + remember(layoutImpl, key, sceneOrOverlay, contentScope, boxScope) { + MovableElementScopeImpl(layoutImpl, key, sceneOrOverlay, contentScope, boxScope) } content(elementScope) @@ -71,7 +72,7 @@ internal fun MovableElement( private abstract class BaseElementScope<ContentScope>( private val layoutImpl: SceneTransitionLayoutImpl, private val element: ElementKey, - private val scene: Scene, + private val sceneOrOverlay: Content, ) : ElementScope<ContentScope> { @Composable override fun <T> animateElementValueAsState( @@ -82,7 +83,7 @@ private abstract class BaseElementScope<ContentScope>( ): AnimatedState<T> { return animateSharedValueAsState( layoutImpl, - scene.key, + sceneOrOverlay.key, element, key, value, @@ -95,12 +96,12 @@ private abstract class BaseElementScope<ContentScope>( private class ElementScopeImpl( layoutImpl: SceneTransitionLayoutImpl, element: ElementKey, - scene: Scene, - private val sceneScope: SceneScope, + content: Content, + private val delegateContentScope: ContentScope, private val boxScope: BoxScope, -) : BaseElementScope<ElementContentScope>(layoutImpl, element, scene) { +) : BaseElementScope<ElementContentScope>(layoutImpl, element, content) { private val contentScope = - object : ElementContentScope, SceneScope by sceneScope, BoxScope by boxScope {} + object : ElementContentScope, ContentScope by delegateContentScope, BoxScope by boxScope {} @Composable override fun content(content: @Composable ElementContentScope.() -> Unit) { @@ -111,12 +112,15 @@ private class ElementScopeImpl( private class MovableElementScopeImpl( private val layoutImpl: SceneTransitionLayoutImpl, private val element: ElementKey, - private val scene: Scene, - private val sceneScope: BaseSceneScope, + private val content: Content, + private val baseContentScope: BaseContentScope, private val boxScope: BoxScope, -) : BaseElementScope<MovableElementContentScope>(layoutImpl, element, scene) { +) : BaseElementScope<MovableElementContentScope>(layoutImpl, element, content) { private val contentScope = - object : MovableElementContentScope, BaseSceneScope by sceneScope, BoxScope by boxScope {} + object : + MovableElementContentScope, + BaseContentScope by baseContentScope, + BoxScope by boxScope {} @Composable override fun content(content: @Composable MovableElementContentScope.() -> Unit) { @@ -126,9 +130,10 @@ private class MovableElementScopeImpl( // during the transition. // TODO(b/317026105): Use derivedStateOf only if the scene picker reads the progress in its // logic. + val contentKey = this@MovableElementScopeImpl.content.key val shouldComposeMovableElement by - remember(layoutImpl, scene.key, element) { - derivedStateOf { shouldComposeMovableElement(layoutImpl, scene.key, element) } + remember(layoutImpl, contentKey, element) { + derivedStateOf { shouldComposeMovableElement(layoutImpl, contentKey, element) } } if (shouldComposeMovableElement) { @@ -152,7 +157,7 @@ private class MovableElementScopeImpl( val size = placeholderContentSize( layoutImpl, - scene.key, + contentKey, layoutImpl.elements.getValue(element), ) layout(size.width, size.height) {} @@ -163,7 +168,7 @@ private class MovableElementScopeImpl( private fun shouldComposeMovableElement( layoutImpl: SceneTransitionLayoutImpl, - scene: SceneKey, + content: ContentKey, element: ElementKey, ): Boolean { val transitions = layoutImpl.state.currentTransitions @@ -171,7 +176,7 @@ private fun shouldComposeMovableElement( // If we are idle, there is only one [scene] that is composed so we can compose our // movable content here. We still check that [scene] is equal to the current idle scene, to // make sure we only compose it there. - return layoutImpl.state.transitionState.currentScene == scene + return layoutImpl.state.transitionState.currentScene == content } // The current transition for this element is the last transition in which either fromScene or @@ -189,7 +194,7 @@ private fun shouldComposeMovableElement( // Always compose movable elements in the scene picked by their scene picker. return shouldPlaceOrComposeSharedElement( layoutImpl, - scene, + content, element, transition, ) @@ -201,12 +206,12 @@ private fun shouldComposeMovableElement( */ private fun placeholderContentSize( layoutImpl: SceneTransitionLayoutImpl, - scene: SceneKey, + content: ContentKey, element: Element, ): IntSize { // If the content of the movable element was already composed in this scene before, use that // target size. - val targetValueInScene = element.sceneStates.getValue(scene).targetSize + val targetValueInScene = element.stateByContent.getValue(content).targetSize if (targetValueInScene != Element.SizeUnspecified) { return targetValueInScene } @@ -219,8 +224,9 @@ private fun placeholderContentSize( // doesn't change between scenes. // TODO(b/317026105): Provide a way to give a hint size/content for cases where this is not // true. - val otherScene = if (transition.fromScene == scene) transition.toScene else transition.fromScene - val targetValueInOtherScene = element.sceneStates[otherScene]?.targetSize + val otherScene = + if (transition.fromScene == content) transition.toScene else transition.fromScene + val targetValueInOtherScene = element.stateByContent[otherScene]?.targetSize if (targetValueInOtherScene != null && targetValueInOtherScene != Element.SizeUnspecified) { return targetValueInOtherScene } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index 2fc4526b31f2..3401af827d4c 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -34,7 +34,6 @@ 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.UserAction.Resolved /** * [SceneTransitionLayout] is a container that automatically animates its content whenever its state @@ -85,7 +84,7 @@ interface SceneTransitionLayoutScope { fun scene( key: SceneKey, userActions: Map<UserAction, UserActionResult> = emptyMap(), - content: @Composable SceneScope.() -> Unit, + content: @Composable ContentScope.() -> Unit, ) } @@ -118,25 +117,25 @@ interface ElementStateScope { @Stable @ElementDsl -interface BaseSceneScope : ElementStateScope { - /** The key of this scene. */ - val sceneKey: SceneKey +interface BaseContentScope : ElementStateScope { + /** The key of this content. */ + val contentKey: ContentKey - /** The state of the [SceneTransitionLayout] in which this scene is contained. */ + /** The state of the [SceneTransitionLayout] in which this content is contained. */ val layoutState: SceneTransitionLayoutState /** * Tag an element identified by [key]. * * Tagging an element will allow you to reference that element when defining transitions, so - * that the element can be transformed and animated when the scene transitions in or out. + * that the element can be transformed and animated when the content transitions in or out. * - * Additionally, this [key] will be used to detect elements that are shared between scenes to + * Additionally, this [key] will be used to detect elements that are shared between contents to * automatically interpolate their size and offset. If you need to animate shared element values - * (i.e. values associated to this element that change depending on which scene it is composed + * (i.e. values associated to this element that change depending on which content it is composed * in), use [Element] instead. * - * Note that shared elements tagged using this function will be duplicated in each scene they + * Note that shared elements tagged using this function will be duplicated in each content they * are part of, so any **internal** state (e.g. state created using `remember { * mutableStateOf(...) }`) will be lost. If you need to preserve internal state, you should use * [MovableElement] instead. @@ -150,7 +149,7 @@ interface BaseSceneScope : ElementStateScope { * Create an element identified by [key]. * * Similar to [element], this creates an element that will be automatically shared when present - * in multiple scenes and that can be transformed during transitions, the same way that + * in multiple contents and that can be transformed during transitions, the same way that * [element] does. * * The only difference with [element] is that the provided [ElementScope] allows you to @@ -177,7 +176,7 @@ interface BaseSceneScope : ElementStateScope { * Create a *movable* element identified by [key]. * * Similar to [Element], this creates an element that will be automatically shared when present - * in multiple scenes and that can be transformed during transitions, and you can also use the + * in multiple contents and that can be transformed during transitions, and you can also use the * provided [ElementScope] to [animate element values][ElementScope.animateElementValueAsState]. * * The important difference with [element] and [Element] is that this element @@ -232,24 +231,26 @@ interface BaseSceneScope : ElementStateScope { fun Modifier.noResizeDuringTransitions(): Modifier } +typealias SceneScope = ContentScope + @Stable @ElementDsl -interface SceneScope : BaseSceneScope { +interface ContentScope : BaseContentScope { /** - * Animate some value at the scene level. + * Animate some value at the content level. * * @param value the value of this shared value in the current scene. * @param key the key of this shared value. * @param type the [SharedValueType] of this animated value. * @param canOverflow whether this value can overflow past the values it is interpolated * between, for instance because the transition is animated using a bouncy spring. - * @see animateSceneIntAsState - * @see animateSceneFloatAsState - * @see animateSceneDpAsState - * @see animateSceneColorAsState + * @see animateContentIntAsState + * @see animateContentFloatAsState + * @see animateContentDpAsState + * @see animateContentColorAsState */ @Composable - fun <T> animateSceneValueAsState( + fun <T> animateContentValueAsState( value: T, key: ValueKey, type: SharedValueType<T, *>, @@ -259,7 +260,7 @@ interface SceneScope : BaseSceneScope { /** * The type of a shared value animated using [ElementScope.animateElementValueAsState] or - * [SceneScope.animateSceneValueAsState]. + * [ContentScope.animateContentValueAsState]. */ @Stable interface SharedValueType<T, Delta> { @@ -321,8 +322,9 @@ interface ElementScope<ContentScope> { * The exact same scope as [androidx.compose.foundation.layout.BoxScope]. * * We can't reuse BoxScope directly because of the @LayoutScopeMarker annotation on it, which would - * prevent us from calling Modifier.element() and other methods of [SceneScope] inside any Box {} in - * the [content][ElementScope.content] of a [SceneScope.Element] or a [SceneScope.MovableElement]. + * prevent us from calling Modifier.element() and other methods of [ContentScope] inside any Box {} + * in the [content][ElementScope.content] of a [ContentScope.Element] or a + * [ContentScope.MovableElement]. */ @Stable @ElementDsl @@ -335,16 +337,16 @@ interface ElementBoxScope { } /** The scope for "normal" (not movable) elements. */ -@Stable @ElementDsl interface ElementContentScope : SceneScope, ElementBoxScope +@Stable @ElementDsl interface ElementContentScope : ContentScope, ElementBoxScope /** * The scope for the content of movable elements. * - * Note that it extends [BaseSceneScope] and not [SceneScope] because movable elements should not - * call [SceneScope.animateSceneValueAsState], given that their content is not composed in all - * scenes. + * Note that it extends [BaseContentScope] and not [ContentScope] because movable elements should + * not call [ContentScope.animateContentValueAsState], given that their content is not composed in + * all scenes. */ -@Stable @ElementDsl interface MovableElementContentScope : BaseSceneScope, ElementBoxScope +@Stable @ElementDsl interface MovableElementContentScope : BaseContentScope, ElementBoxScope /** An action performed by the user. */ sealed class UserAction { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt index 32db0b7cd9fe..062d5533c539 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt @@ -36,6 +36,8 @@ import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEachReversed +import com.android.compose.animation.scene.content.Content +import com.android.compose.animation.scene.content.Scene import com.android.compose.ui.util.lerp import kotlinx.coroutines.CoroutineScope @@ -84,7 +86,7 @@ internal class SceneTransitionLayoutImpl( /** * The different values of a shared value keyed by a a [ValueKey] and the different elements and - * scenes it is associated to. + * contents it is associated to. */ private var _sharedValues: MutableMap<ValueKey, MutableMap<ElementKey?, SharedValue<*, *>>>? = null @@ -149,6 +151,12 @@ internal class SceneTransitionLayoutImpl( return scenes[key] ?: error("Scene $key is not configured") } + internal fun content(key: ContentKey): Content { + return when (key) { + is SceneKey -> scene(key) + } + } + internal fun updateScenes( builder: SceneTransitionLayoutScope.() -> Unit, layoutDirection: LayoutDirection, @@ -164,7 +172,7 @@ internal class SceneTransitionLayoutImpl( override fun scene( key: SceneKey, userActions: Map<UserAction, UserActionResult>, - content: @Composable SceneScope.() -> Unit, + content: @Composable ContentScope.() -> Unit, ) { scenesToRemove.remove(key) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt index 06b093d0b5db..cfa4c70c8239 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt @@ -302,18 +302,18 @@ internal class TransformationSpecImpl( override val distance: UserActionDistance?, override val transformations: List<Transformation>, ) : TransformationSpec { - private val cache = mutableMapOf<ElementKey, MutableMap<SceneKey, ElementTransformations>>() + private val cache = mutableMapOf<ElementKey, MutableMap<ContentKey, ElementTransformations>>() - internal fun transformations(element: ElementKey, scene: SceneKey): ElementTransformations { + internal fun transformations(element: ElementKey, content: ContentKey): ElementTransformations { return cache .getOrPut(element) { mutableMapOf() } - .getOrPut(scene) { computeTransformations(element, scene) } + .getOrPut(content) { computeTransformations(element, content) } } /** Filter [transformations] to compute the [ElementTransformations] of [element]. */ private fun computeTransformations( element: ElementKey, - scene: SceneKey, + content: ContentKey, ): ElementTransformations { var shared: SharedElementTransformation? = null var offset: PropertyTransformation<Offset>? = null @@ -351,7 +351,7 @@ internal class TransformationSpecImpl( } transformations.fastForEach { transformation -> - if (!transformation.matcher.matches(element, scene)) { + if (!transformation.matcher.matches(element, content)) { return@fastForEach } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt index a2118b2ff5bb..f06214645144 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.node.PointerInputModifierNode import androidx.compose.ui.node.TraversableNode import androidx.compose.ui.node.findNearestAncestor import androidx.compose.ui.unit.IntSize +import com.android.compose.animation.scene.content.Scene /** * Configures the swipeable behavior of a [SceneTransitionLayout] depending on the current state. diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt index 3a87d4130cfb..06be86d8eaf7 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt @@ -239,10 +239,11 @@ interface ElementScenePicker { * should not be drawn or composed in neither [transition.fromScene] nor [transition.toScene], * return `null`. * - * Important: For [MovableElements][SceneScope.MovableElement], this scene picker will *always* - * be used during transitions to decide whether we should compose that element in a given scene - * or not. Therefore, you should make sure that the returned [SceneKey] contains the movable - * element, otherwise that element will not be composed in any scene during the transition. + * Important: For [MovableElements][ContentScope.MovableElement], this scene picker will + * *always* be used during transitions to decide whether we should compose that element in a + * given scene or not. Therefore, you should make sure that the returned [SceneKey] contains the + * movable element, otherwise that element will not be composed in any scene during the + * transition. */ fun sceneDuringTransition( element: ElementKey, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt index b7abb33c1242..0f668044112e 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt @@ -23,13 +23,13 @@ internal class ElementStateScopeImpl( private val layoutImpl: SceneTransitionLayoutImpl, ) : ElementStateScope { override fun ElementKey.targetSize(scene: SceneKey): IntSize? { - return layoutImpl.elements[this]?.sceneStates?.get(scene)?.targetSize.takeIf { + return layoutImpl.elements[this]?.stateByContent?.get(scene)?.targetSize.takeIf { it != Element.SizeUnspecified } } override fun ElementKey.targetOffset(scene: SceneKey): Offset? { - return layoutImpl.elements[this]?.sceneStates?.get(scene)?.targetOffset.takeIf { + return layoutImpl.elements[this]?.stateByContent?.get(scene)?.targetOffset.takeIf { it != Offset.Unspecified } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt index a49f1af97183..492d2115cfdc 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 The Android Open Source Project + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.compose.animation.scene +package com.android.compose.animation.scene.content import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Box @@ -29,24 +29,44 @@ import androidx.compose.ui.layout.approachLayout import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.IntSize import androidx.compose.ui.zIndex +import com.android.compose.animation.scene.AnimatedState +import com.android.compose.animation.scene.ContentKey +import com.android.compose.animation.scene.ContentScope +import com.android.compose.animation.scene.Element +import com.android.compose.animation.scene.ElementContentScope +import com.android.compose.animation.scene.ElementKey +import com.android.compose.animation.scene.ElementScope +import com.android.compose.animation.scene.ElementStateScope +import com.android.compose.animation.scene.MovableElement +import com.android.compose.animation.scene.MovableElementContentScope +import com.android.compose.animation.scene.NestedScrollBehavior +import com.android.compose.animation.scene.SceneTransitionLayoutImpl +import com.android.compose.animation.scene.SceneTransitionLayoutState +import com.android.compose.animation.scene.SharedValueType +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult +import com.android.compose.animation.scene.ValueKey +import com.android.compose.animation.scene.animateSharedValueAsState +import com.android.compose.animation.scene.element import com.android.compose.animation.scene.modifiers.noResizeDuringTransitions +import com.android.compose.animation.scene.nestedScrollToScene -/** A scene in a [SceneTransitionLayout]. */ +/** A content defined in a [SceneTransitionLayout], i.e. a scene or an overlay. */ @Stable -internal class Scene( - val key: SceneKey, - layoutImpl: SceneTransitionLayoutImpl, - content: @Composable SceneScope.() -> Unit, +internal sealed class Content( + open val key: ContentKey, + val layoutImpl: SceneTransitionLayoutImpl, + content: @Composable ContentScope.() -> Unit, actions: Map<UserAction.Resolved, UserActionResult>, zIndex: Float, ) { - internal val scope = SceneScopeImpl(layoutImpl, this) + internal val scope = ContentScopeImpl(layoutImpl, content = this) var content by mutableStateOf(content) - private var _userActions by mutableStateOf(checkValid(actions)) var zIndex by mutableFloatStateOf(zIndex) var targetSize by mutableStateOf(IntSize.Zero) + private var _userActions by mutableStateOf(checkValid(actions)) var userActions get() = _userActions set(value) { @@ -59,8 +79,8 @@ internal class Scene( userActions.forEach { (action, result) -> if (key == result.toScene) { error( - "Transition to the same scene is not supported. Scene $key, action $action," + - " result $result" + "Transition to the same content (scene/overlay) is not supported. Content " + + "$key, action $action, result $result" ) } } @@ -73,7 +93,7 @@ internal class Scene( modifier .zIndex(zIndex) .approachLayout( - isMeasurementApproachInProgress = { scope.layoutState.isTransitioning() } + isMeasurementApproachInProgress = { layoutImpl.state.isTransitioning() } ) { measurable, constraints -> targetSize = lookaheadSize val placeable = measurable.measure(constraints) @@ -84,21 +104,19 @@ internal class Scene( scope.content() } } - - override fun toString(): String { - return "Scene(key=$key)" - } } -internal class SceneScopeImpl( +internal class ContentScopeImpl( private val layoutImpl: SceneTransitionLayoutImpl, - private val scene: Scene, -) : SceneScope, ElementStateScope by layoutImpl.elementStateScope { - override val sceneKey: SceneKey = scene.key + private val content: Content, +) : ContentScope, ElementStateScope by layoutImpl.elementStateScope { + override val contentKey: ContentKey + get() = content.key + override val layoutState: SceneTransitionLayoutState = layoutImpl.state override fun Modifier.element(key: ElementKey): Modifier { - return element(layoutImpl, scene, key) + return element(layoutImpl, content, key) } @Composable @@ -107,7 +125,7 @@ internal class SceneScopeImpl( modifier: Modifier, content: @Composable (ElementScope<ElementContentScope>.() -> Unit) ) { - Element(layoutImpl, scene, key, modifier, content) + Element(layoutImpl, this@ContentScopeImpl.content, key, modifier, content) } @Composable @@ -116,19 +134,19 @@ internal class SceneScopeImpl( modifier: Modifier, content: @Composable (ElementScope<MovableElementContentScope>.() -> Unit) ) { - MovableElement(layoutImpl, scene, key, modifier, content) + MovableElement(layoutImpl, this@ContentScopeImpl.content, key, modifier, content) } @Composable - override fun <T> animateSceneValueAsState( + override fun <T> animateContentValueAsState( value: T, key: ValueKey, type: SharedValueType<T, *>, - canOverflow: Boolean + canOverflow: Boolean, ): AnimatedState<T> { return animateSharedValueAsState( layoutImpl = layoutImpl, - scene = scene.key, + content = content.key, element = null, key = key, value = value, @@ -141,27 +159,29 @@ internal class SceneScopeImpl( leftBehavior: NestedScrollBehavior, rightBehavior: NestedScrollBehavior, isExternalOverscrollGesture: () -> Boolean, - ): Modifier = - nestedScrollToScene( + ): Modifier { + return nestedScrollToScene( layoutImpl = layoutImpl, orientation = Orientation.Horizontal, topOrLeftBehavior = leftBehavior, bottomOrRightBehavior = rightBehavior, isExternalOverscrollGesture = isExternalOverscrollGesture, ) + } override fun Modifier.verticalNestedScrollToScene( topBehavior: NestedScrollBehavior, bottomBehavior: NestedScrollBehavior, isExternalOverscrollGesture: () -> Boolean, - ): Modifier = - nestedScrollToScene( + ): Modifier { + return nestedScrollToScene( layoutImpl = layoutImpl, orientation = Orientation.Vertical, topOrLeftBehavior = topBehavior, bottomOrRightBehavior = bottomBehavior, isExternalOverscrollGesture = isExternalOverscrollGesture, ) + } override fun Modifier.noResizeDuringTransitions(): Modifier { return noResizeDuringTransitions(layoutState = layoutImpl.state) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Scene.kt new file mode 100644 index 000000000000..4a7a94d6e177 --- /dev/null +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Scene.kt @@ -0,0 +1,39 @@ +/* + * 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.compose.animation.scene.content + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import com.android.compose.animation.scene.ContentScope +import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.SceneTransitionLayoutImpl +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult + +/** A scene defined in a [SceneTransitionLayout]. */ +@Stable +internal class Scene( + override val key: SceneKey, + layoutImpl: SceneTransitionLayoutImpl, + content: @Composable ContentScope.() -> Unit, + actions: Map<UserAction.Resolved, UserActionResult>, + zIndex: Float, +) : Content(key, layoutImpl, content, actions, zIndex) { + override fun toString(): String { + return "Scene(key=$key)" + } +} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt index 73ee4512c31f..65d4d2da4dd1 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt @@ -17,6 +17,7 @@ package com.android.compose.animation.scene.transformation import androidx.compose.ui.unit.IntSize +import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.Element import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.ElementMatcher @@ -33,15 +34,15 @@ internal class AnchoredSize( ) : PropertyTransformation<IntSize> { override fun transform( layoutImpl: SceneTransitionLayoutImpl, - scene: SceneKey, + content: ContentKey, element: Element, - sceneState: Element.SceneState, + sceneState: Element.State, transition: TransitionState.Transition, value: IntSize, ): IntSize { fun anchorSizeIn(scene: SceneKey): IntSize { val size = - layoutImpl.elements[anchor]?.sceneStates?.get(scene)?.targetSize?.takeIf { + layoutImpl.elements[anchor]?.stateByContent?.get(scene)?.targetSize?.takeIf { it != Element.SizeUnspecified } ?: throwMissingAnchorException( @@ -59,7 +60,7 @@ internal class AnchoredSize( // This simple implementation assumes that the size of [element] is the same as the size of // the [anchor] in [scene], so simply transform to the size of the anchor in the other // scene. - return if (scene == transition.fromScene) { + return if (content == transition.fromScene) { anchorSizeIn(transition.toScene) } else { anchorSizeIn(transition.fromScene) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt index 70dca4c065d3..8d7e1c971acf 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt @@ -18,6 +18,7 @@ package com.android.compose.animation.scene.transformation import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.isSpecified +import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.Element import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.ElementMatcher @@ -32,9 +33,9 @@ internal class AnchoredTranslate( ) : PropertyTransformation<Offset> { override fun transform( layoutImpl: SceneTransitionLayoutImpl, - scene: SceneKey, + content: ContentKey, element: Element, - sceneState: Element.SceneState, + sceneState: Element.State, transition: TransitionState.Transition, value: Offset, ): Offset { @@ -48,7 +49,7 @@ internal class AnchoredTranslate( val anchor = layoutImpl.elements[anchor] ?: throwException(scene = null) fun anchorOffsetIn(scene: SceneKey): Offset? { - return anchor.sceneStates[scene]?.targetOffset?.takeIf { it.isSpecified } + return anchor.stateByContent[scene]?.targetOffset?.takeIf { it.isSpecified } } // [element] will move the same amount as [anchor] does. @@ -60,7 +61,7 @@ internal class AnchoredTranslate( anchorOffsetIn(transition.toScene) ?: throwException(transition.toScene) val offset = anchorToOffset - anchorFromOffset - return if (scene == transition.toScene) { + return if (content == transition.toScene) { Offset( value.x - offset.x, value.y - offset.y, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt index 98c2dd3dc1cc..f010c3b1d3b4 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt @@ -17,10 +17,10 @@ package com.android.compose.animation.scene.transformation import androidx.compose.ui.geometry.Offset +import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.Element import com.android.compose.animation.scene.ElementMatcher import com.android.compose.animation.scene.Scale -import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.SceneTransitionLayoutImpl import com.android.compose.animation.scene.TransitionState @@ -37,9 +37,9 @@ internal class DrawScale( override fun transform( layoutImpl: SceneTransitionLayoutImpl, - scene: SceneKey, + content: ContentKey, element: Element, - sceneState: Element.SceneState, + sceneState: Element.State, transition: TransitionState.Transition, value: Scale, ): Scale { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt index 7daefd0d5d77..dfce997ba190 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt @@ -17,10 +17,10 @@ package com.android.compose.animation.scene.transformation import androidx.compose.ui.geometry.Offset +import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.Element import com.android.compose.animation.scene.ElementMatcher -import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.SceneTransitionLayoutImpl import com.android.compose.animation.scene.TransitionState @@ -32,13 +32,13 @@ internal class EdgeTranslate( ) : PropertyTransformation<Offset> { override fun transform( layoutImpl: SceneTransitionLayoutImpl, - scene: SceneKey, + content: ContentKey, element: Element, - sceneState: Element.SceneState, + sceneState: Element.State, transition: TransitionState.Transition, value: Offset ): Offset { - val sceneSize = layoutImpl.scene(scene).targetSize + val sceneSize = layoutImpl.content(content).targetSize val elementSize = sceneState.targetSize if (elementSize == Element.SizeUnspecified) { return value diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt index ada814e04ab2..c1bb017143a9 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt @@ -16,9 +16,9 @@ package com.android.compose.animation.scene.transformation +import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.Element import com.android.compose.animation.scene.ElementMatcher -import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.SceneTransitionLayoutImpl import com.android.compose.animation.scene.TransitionState @@ -28,9 +28,9 @@ internal class Fade( ) : PropertyTransformation<Float> { override fun transform( layoutImpl: SceneTransitionLayoutImpl, - scene: SceneKey, + content: ContentKey, element: Element, - sceneState: Element.SceneState, + sceneState: Element.State, transition: TransitionState.Transition, value: Float ): Float { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt index dca8f8521f1a..5adbf7eb2614 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt @@ -17,9 +17,9 @@ package com.android.compose.animation.scene.transformation import androidx.compose.ui.unit.IntSize +import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.Element import com.android.compose.animation.scene.ElementMatcher -import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.SceneTransitionLayoutImpl import com.android.compose.animation.scene.TransitionState import kotlin.math.roundToInt @@ -35,9 +35,9 @@ internal class ScaleSize( ) : PropertyTransformation<IntSize> { override fun transform( layoutImpl: SceneTransitionLayoutImpl, - scene: SceneKey, + content: ContentKey, element: Element, - sceneState: Element.SceneState, + sceneState: Element.State, transition: TransitionState.Transition, value: IntSize, ): IntSize { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt index 7be9ce1e39fc..24b71944b6d0 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt @@ -19,9 +19,9 @@ package com.android.compose.animation.scene.transformation import androidx.compose.ui.util.fastCoerceAtLeast import androidx.compose.ui.util.fastCoerceAtMost import androidx.compose.ui.util.fastCoerceIn +import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.Element import com.android.compose.animation.scene.ElementMatcher -import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.SceneTransitionLayoutImpl import com.android.compose.animation.scene.TransitionState @@ -61,9 +61,9 @@ internal sealed interface PropertyTransformation<T> : Transformation { // to these internal classes. fun transform( layoutImpl: SceneTransitionLayoutImpl, - scene: SceneKey, + content: ContentKey, element: Element, - sceneState: Element.SceneState, + sceneState: Element.State, transition: TransitionState.Transition, value: T, ): T diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt index f066511f68ab..123756ae4211 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt @@ -19,10 +19,10 @@ package com.android.compose.animation.scene.transformation import androidx.compose.ui.geometry.Offset import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.Element import com.android.compose.animation.scene.ElementMatcher import com.android.compose.animation.scene.OverscrollScope -import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.SceneTransitionLayoutImpl import com.android.compose.animation.scene.TransitionState @@ -33,9 +33,9 @@ internal class Translate( ) : PropertyTransformation<Offset> { override fun transform( layoutImpl: SceneTransitionLayoutImpl, - scene: SceneKey, + content: ContentKey, element: Element, - sceneState: Element.SceneState, + sceneState: Element.State, transition: TransitionState.Transition, value: Offset, ): Offset { @@ -55,9 +55,9 @@ internal class OverscrollTranslate( ) : PropertyTransformation<Offset> { override fun transform( layoutImpl: SceneTransitionLayoutImpl, - scene: SceneKey, + content: ContentKey, element: Element, - sceneState: Element.SceneState, + sceneState: Element.State, transition: TransitionState.Transition, value: Offset, ): Offset { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt index 8e35988832dc..ae3169b117ba 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt @@ -57,7 +57,7 @@ fun LargeTopAppBarNestedScrollConnection( minHeight() < currentHeight && currentHeight < maxHeight() }, canScrollOnFling = true, - onStart = { /* do nothing */}, + onStart = { /* do nothing */ }, onScroll = { offsetAvailable -> val currentHeight = height() val amountConsumed = diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt index ac11d3040d67..228f7ba48d3e 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt @@ -38,7 +38,7 @@ class PriorityNestedScrollConnection( private val canStartPreScroll: (offsetAvailable: Offset, offsetBeforeStart: Offset) -> Boolean, private val canStartPostScroll: (offsetAvailable: Offset, offsetBeforeStart: Offset) -> Boolean, private val canStartPostFling: (velocityAvailable: Velocity) -> Boolean, - private val canContinueScroll: () -> Boolean, + private val canContinueScroll: (source: NestedScrollSource) -> Boolean, private val canScrollOnFling: Boolean, private val onStart: (offsetAvailable: Offset) -> Unit, private val onScroll: (offsetAvailable: Offset) -> Offset, @@ -61,7 +61,7 @@ class PriorityNestedScrollConnection( if ( isPriorityMode || - (source == NestedScrollSource.Fling && !canScrollOnFling) || + (source == NestedScrollSource.SideEffect && !canScrollOnFling) || !canStartPostScroll(available, offsetBeforeStart) ) { // The priority mode cannot start so we won't consume the available offset. @@ -73,7 +73,7 @@ class PriorityNestedScrollConnection( override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { if (!isPriorityMode) { - if (source != NestedScrollSource.Fling || canScrollOnFling) { + if (source == NestedScrollSource.UserInput || canScrollOnFling) { if (canStartPreScroll(available, offsetScrolledBeforePriorityMode)) { return onPriorityStart(available) } @@ -84,7 +84,7 @@ class PriorityNestedScrollConnection( return Offset.Zero } - if (!canContinueScroll()) { + if (!canContinueScroll(source)) { // Step 3a: We have lost priority and we no longer need to intercept scroll events. onPriorityStop(velocity = Velocity.Zero) @@ -170,7 +170,7 @@ fun PriorityNestedScrollConnection( canStartPreScroll: (offsetAvailable: Float, offsetBeforeStart: Float) -> Boolean, canStartPostScroll: (offsetAvailable: Float, offsetBeforeStart: Float) -> Boolean, canStartPostFling: (velocityAvailable: Float) -> Boolean, - canContinueScroll: () -> Boolean, + canContinueScroll: (source: NestedScrollSource) -> Boolean, canScrollOnFling: Boolean, onStart: (offsetAvailable: Float) -> Unit, onScroll: (offsetAvailable: Float) -> Float, diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt index a7889e2fac58..0f33303dcd15 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt @@ -67,7 +67,7 @@ class AnimatedSharedAsStateTest { } @Composable - private fun SceneScope.Foo( + private fun ContentScope.Foo( targetValues: Values, onCurrentValueChanged: (Values) -> Unit, ) { @@ -87,7 +87,7 @@ class AnimatedSharedAsStateTest { } @Composable - private fun SceneScope.MovableFoo( + private fun ContentScope.MovableFoo( targetValues: Values, onCurrentValueChanged: (Values) -> Unit, ) { @@ -105,14 +105,14 @@ class AnimatedSharedAsStateTest { } @Composable - private fun SceneScope.SceneValues( + private fun ContentScope.SceneValues( targetValues: Values, onCurrentValueChanged: (Values) -> Unit, ) { - val int by animateSceneIntAsState(targetValues.int, key = TestValues.Value1) - val float by animateSceneFloatAsState(targetValues.float, key = TestValues.Value2) - val dp by animateSceneDpAsState(targetValues.dp, key = TestValues.Value3) - val color by animateSceneColorAsState(targetValues.color, key = TestValues.Value4) + val int by animateContentIntAsState(targetValues.int, key = TestValues.Value1) + val float by animateContentFloatAsState(targetValues.float, key = TestValues.Value2) + val dp by animateContentDpAsState(targetValues.dp, key = TestValues.Value3) + val color by animateContentColorAsState(targetValues.color, key = TestValues.Value4) LaunchedEffect(Unit) { snapshotFlow { Values(int, float, dp, color) }.collect(onCurrentValueChanged) @@ -292,7 +292,7 @@ class AnimatedSharedAsStateTest { fun readingAnimatedStateValueDuringCompositionThrows() { assertThrows(IllegalStateException::class.java) { rule.testTransition( - fromSceneContent = { animateSceneIntAsState(0, TestValues.Value1).value }, + fromSceneContent = { animateContentIntAsState(0, TestValues.Value1).value }, toSceneContent = {}, transition = {}, ) {} @@ -302,21 +302,21 @@ class AnimatedSharedAsStateTest { @Test fun readingAnimatedStateValueDuringCompositionIsStillPossible() { @Composable - fun SceneScope.SceneValuesDuringComposition( + fun ContentScope.SceneValuesDuringComposition( targetValues: Values, onCurrentValueChanged: (Values) -> Unit, ) { val int by - animateSceneIntAsState(targetValues.int, key = TestValues.Value1) + animateContentIntAsState(targetValues.int, key = TestValues.Value1) .unsafeCompositionState(targetValues.int) val float by - animateSceneFloatAsState(targetValues.float, key = TestValues.Value2) + animateContentFloatAsState(targetValues.float, key = TestValues.Value2) .unsafeCompositionState(targetValues.float) val dp by - animateSceneDpAsState(targetValues.dp, key = TestValues.Value3) + animateContentDpAsState(targetValues.dp, key = TestValues.Value3) .unsafeCompositionState(targetValues.dp) val color by - animateSceneColorAsState(targetValues.color, key = TestValues.Value4) + animateContentColorAsState(targetValues.color, key = TestValues.Value4) .unsafeCompositionState(targetValues.color) val values = Values(int, float, dp, color) @@ -397,14 +397,14 @@ class AnimatedSharedAsStateTest { val foo = ValueKey("foo") val bar = ValueKey("bar") - val lastValues = mutableMapOf<ValueKey, MutableMap<SceneKey, Float>>() + val lastValues = mutableMapOf<ValueKey, MutableMap<ContentKey, Float>>() @Composable - fun SceneScope.animateFloat(value: Float, key: ValueKey) { - val animatedValue = animateSceneFloatAsState(value, key) + fun ContentScope.animateFloat(value: Float, key: ValueKey) { + val animatedValue = animateContentFloatAsState(value, key) LaunchedEffect(animatedValue) { snapshotFlow { animatedValue.value } - .collect { lastValues.getOrPut(key) { mutableMapOf() }[sceneKey] = it } + .collect { lastValues.getOrPut(key) { mutableMapOf() }[contentKey] = it } } } @@ -458,13 +458,13 @@ class AnimatedSharedAsStateTest { } val key = ValueKey("foo") - val lastValues = mutableMapOf<SceneKey, Float>() + val lastValues = mutableMapOf<ContentKey, Float>() @Composable - fun SceneScope.animateFloat(value: Float, key: ValueKey) { - val animatedValue = animateSceneFloatAsState(value, key) + fun ContentScope.animateFloat(value: Float, key: ValueKey) { + val animatedValue = animateContentFloatAsState(value, key) LaunchedEffect(animatedValue) { - snapshotFlow { animatedValue.value }.collect { lastValues[sceneKey] = it } + snapshotFlow { animatedValue.value }.collect { lastValues[contentKey] = it } } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt index 1d9e9b72cc33..329257e4be22 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt @@ -86,7 +86,7 @@ class ElementTest { @get:Rule val rule = createComposeRule() @Composable - private fun SceneScope.Element( + private fun ContentScope.Element( key: ElementKey, size: Dp, offset: Dp, @@ -380,7 +380,7 @@ class ElementTest { assertThat(layoutImpl.elements.keys).containsExactly(key) val element = layoutImpl.elements.getValue(key) - assertThat(element.sceneStates.keys).containsExactly(SceneB) + assertThat(element.stateByContent.keys).containsExactly(SceneB) // Scene C, state 0: the same element is reused. rule.runOnUiThread { state.setTargetScene(SceneC, coroutineScope) } @@ -389,13 +389,13 @@ class ElementTest { assertThat(layoutImpl.elements.keys).containsExactly(key) assertThat(layoutImpl.elements.getValue(key)).isSameInstanceAs(element) - assertThat(element.sceneStates.keys).containsExactly(SceneC) + assertThat(element.stateByContent.keys).containsExactly(SceneC) // Scene C, state 1: the element is removed from the map. sceneCState = 1 rule.waitForIdle() - assertThat(element.sceneStates).isEmpty() + assertThat(element.stateByContent).isEmpty() assertThat(layoutImpl.elements).isEmpty() } @@ -405,7 +405,7 @@ class ElementTest { assertThrows(IllegalStateException::class.java) { rule.setContent { - TestSceneScope { + TestContentScope { Column { Box(Modifier.element(key)) Box(Modifier.element(key)) @@ -421,7 +421,7 @@ class ElementTest { assertThrows(IllegalStateException::class.java) { rule.setContent { - TestSceneScope { + TestContentScope { Column { val childModifier = Modifier.element(key) Box(childModifier) @@ -439,7 +439,7 @@ class ElementTest { assertThrows(IllegalStateException::class.java) { var nElements by mutableStateOf(1) rule.setContent { - TestSceneScope { + TestContentScope { Column { val childModifier = Modifier.element(key) repeat(nElements) { Box(childModifier) } @@ -457,7 +457,7 @@ class ElementTest { assertThrows(IllegalStateException::class.java) { var key by mutableStateOf(TestElements.Foo) rule.setContent { - TestSceneScope { + TestContentScope { Column { Box(Modifier.element(key)) Box(Modifier.element(TestElements.Bar)) @@ -491,7 +491,7 @@ class ElementTest { // There is only Foo in the elements map. assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Foo) val fooElement = layoutImpl.elements.getValue(TestElements.Foo) - assertThat(fooElement.sceneStates.keys).containsExactly(SceneA) + assertThat(fooElement.stateByContent.keys).containsExactly(SceneA) key = TestElements.Bar @@ -499,8 +499,8 @@ class ElementTest { rule.waitForIdle() assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Bar) val barElement = layoutImpl.elements.getValue(TestElements.Bar) - assertThat(barElement.sceneStates.keys).containsExactly(SceneA) - assertThat(fooElement.sceneStates).isEmpty() + assertThat(barElement.stateByContent.keys).containsExactly(SceneA) + assertThat(fooElement.stateByContent).isEmpty() } @Test @@ -553,7 +553,7 @@ class ElementTest { // There is only Foo in the elements map. assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Foo) val element = layoutImpl.elements.getValue(TestElements.Foo) - val sceneValues = element.sceneStates + val sceneValues = element.stateByContent assertThat(sceneValues.keys).containsExactly(SceneA) // Get the ElementModifier node that should be reused later on when coming back to this @@ -576,7 +576,7 @@ class ElementTest { assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Foo) val newElement = layoutImpl.elements.getValue(TestElements.Foo) - val newSceneValues = newElement.sceneStates + val newSceneValues = newElement.stateByContent assertThat(newElement).isNotEqualTo(element) assertThat(newSceneValues).isNotEqualTo(sceneValues) assertThat(newSceneValues.keys).containsExactly(SceneA) @@ -677,7 +677,7 @@ class ElementTest { modifier = Modifier.size(layoutWidth, layoutHeight) ) { scene(key = SceneA, userActions = mapOf(Swipe.Down to SceneB)) { - animateSceneFloatAsState( + animateContentFloatAsState( value = animatedFloatRange.start, key = TestValues.Value1, false @@ -686,7 +686,7 @@ class ElementTest { } scene(SceneB) { val animatedFloat by - animateSceneFloatAsState( + animateContentFloatAsState( value = animatedFloatRange.endInclusive, key = TestValues.Value1, canOverflow = false @@ -1215,15 +1215,15 @@ class ElementTest { } val layoutSize = DpSize(200.dp, 100.dp) - val lastValues = mutableMapOf<SceneKey, Float>() + val lastValues = mutableMapOf<ContentKey, Float>() @Composable - fun SceneScope.Foo(size: Dp, value: Float, modifier: Modifier = Modifier) { - val sceneKey = this.sceneKey + fun ContentScope.Foo(size: Dp, value: Float, modifier: Modifier = Modifier) { + val contentKey = this.contentKey Element(TestElements.Foo, modifier.size(size)) { val animatedValue = animateElementFloatAsState(value, TestValues.Value1) LaunchedEffect(animatedValue) { - snapshotFlow { animatedValue.value }.collect { lastValues[sceneKey] = it } + snapshotFlow { animatedValue.value }.collect { lastValues[contentKey] = it } } } } @@ -1388,8 +1388,8 @@ class ElementTest { // The interruption values should be unspecified and deltas should be set to zero. val foo = layoutImpl.elements.getValue(TestElements.Foo) - assertThat(foo.sceneStates.keys).containsExactly(SceneC) - val stateInC = foo.sceneStates.getValue(SceneC) + assertThat(foo.stateByContent.keys).containsExactly(SceneC) + val stateInC = foo.stateByContent.getValue(SceneC) assertThat(stateInC.offsetBeforeInterruption).isEqualTo(Offset.Unspecified) assertThat(stateInC.sizeBeforeInterruption).isEqualTo(Element.SizeUnspecified) assertThat(stateInC.scaleBeforeInterruption).isEqualTo(Scale.Unspecified) @@ -1423,7 +1423,7 @@ class ElementTest { } @Composable - fun SceneScope.Foo(modifier: Modifier = Modifier) { + fun ContentScope.Foo(modifier: Modifier = Modifier) { Box(modifier.element(TestElements.Foo).size(fooSize)) } @@ -1542,8 +1542,8 @@ class ElementTest { assertThat(layoutImpl.elements).containsKey(TestElements.Foo) val foo = layoutImpl.elements.getValue(TestElements.Foo) - assertThat(foo.sceneStates).containsKey(SceneB) - val bState = foo.sceneStates.getValue(SceneB) + assertThat(foo.stateByContent).containsKey(SceneB) + val bState = foo.stateByContent.getValue(SceneB) assertThat(bState.targetSize).isNotEqualTo(Element.SizeUnspecified) assertThat(bState.targetOffset).isNotEqualTo(Offset.Unspecified) @@ -1583,9 +1583,9 @@ class ElementTest { rule.waitForIdle() val foo = checkNotNull(layoutImpl.elements[TestElements.Foo]) - assertThat(foo.sceneStates[SceneA]).isNull() + assertThat(foo.stateByContent[SceneA]).isNull() - val fooInB = foo.sceneStates[SceneB] + val fooInB = foo.stateByContent[SceneB] assertThat(fooInB).isNotNull() assertThat(fooInB!!.lastAlpha).isEqualTo(0.5f) @@ -1599,7 +1599,7 @@ class ElementTest { state.startTransition(transition(from = SceneB, to = SceneC, progress = { 0.3f })) } rule.waitForIdle() - val fooInC = foo.sceneStates[SceneC] + val fooInC = foo.stateByContent[SceneC] assertThat(fooInC).isNotNull() assertThat(fooInC!!.lastAlpha).isEqualTo(1f) assertThat(fooInB.lastAlpha).isEqualTo(Element.AlphaUnspecified) @@ -1645,7 +1645,7 @@ class ElementTest { rule.waitForIdle() // Alpha of Foo should be 0f at interruption progress 100%. - val fooInB = layoutImpl.elements.getValue(TestElements.Foo).sceneStates.getValue(SceneB) + val fooInB = layoutImpl.elements.getValue(TestElements.Foo).stateByContent.getValue(SceneB) assertThat(fooInB.lastAlpha).isEqualTo(0f) // Alpha of Foo should be 0.6f at interruption progress 0%. @@ -1673,7 +1673,7 @@ class ElementTest { } @Composable - fun SceneScope.Foo() { + fun ContentScope.Foo() { Box(Modifier.element(TestElements.Foo).size(10.dp)) } @@ -1724,7 +1724,7 @@ class ElementTest { val fooInB = "fooInB" @Composable - fun SceneScope.MovableFoo(text: String, modifier: Modifier = Modifier) { + fun ContentScope.MovableFoo(text: String, modifier: Modifier = Modifier) { MovableElement(TestElements.Foo, modifier) { content { Text(text) } } } @@ -1773,7 +1773,7 @@ class ElementTest { } @Composable - fun SceneScope.SceneWithFoo(offset: DpOffset, modifier: Modifier = Modifier) { + fun ContentScope.SceneWithFoo(offset: DpOffset, modifier: Modifier = Modifier) { Box(modifier.fillMaxSize()) { Box(Modifier.offset(offset.x, offset.y).element(TestElements.Foo).size(100.dp)) } @@ -1856,7 +1856,7 @@ class ElementTest { val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) } @Composable - fun SceneScope.NestedFooBar() { + fun ContentScope.NestedFooBar() { Box(Modifier.element(TestElements.Foo)) { Box(Modifier.element(TestElements.Bar).size(10.dp)) } @@ -1881,13 +1881,13 @@ class ElementTest { val foo = layoutImpl.elements.getValue(TestElements.Foo) val bar = layoutImpl.elements.getValue(TestElements.Bar) - assertThat(foo.sceneStates).containsKey(SceneA) - assertThat(bar.sceneStates).containsKey(SceneA) - assertThat(foo.sceneStates).doesNotContainKey(SceneB) - assertThat(bar.sceneStates).doesNotContainKey(SceneB) + assertThat(foo.stateByContent).containsKey(SceneA) + assertThat(bar.stateByContent).containsKey(SceneA) + assertThat(foo.stateByContent).doesNotContainKey(SceneB) + assertThat(bar.stateByContent).doesNotContainKey(SceneB) - val fooInA = foo.sceneStates.getValue(SceneA) - val barInA = bar.sceneStates.getValue(SceneA) + val fooInA = foo.stateByContent.getValue(SceneA) + val barInA = bar.stateByContent.getValue(SceneA) assertThat(fooInA.lastOffset).isNotEqualTo(Offset.Unspecified) assertThat(fooInA.lastAlpha).isNotEqualTo(Element.AlphaUnspecified) assertThat(fooInA.lastScale).isNotEqualTo(Scale.Unspecified) @@ -1903,11 +1903,11 @@ class ElementTest { rule.onNode(isElement(TestElements.Foo, SceneB)).assertIsDisplayed() rule.onNode(isElement(TestElements.Bar, SceneB)).assertIsDisplayed() - assertThat(foo.sceneStates).containsKey(SceneB) - assertThat(bar.sceneStates).containsKey(SceneB) + assertThat(foo.stateByContent).containsKey(SceneB) + assertThat(bar.stateByContent).containsKey(SceneB) - val fooInB = foo.sceneStates.getValue(SceneB) - val barInB = bar.sceneStates.getValue(SceneB) + val fooInB = foo.stateByContent.getValue(SceneB) + val barInB = bar.stateByContent.getValue(SceneB) assertThat(fooInA.lastOffset).isEqualTo(Offset.Unspecified) assertThat(fooInA.lastAlpha).isEqualTo(Element.AlphaUnspecified) assertThat(fooInA.lastScale).isEqualTo(Scale.Unspecified) @@ -1938,8 +1938,8 @@ class ElementTest { } @Composable - fun SceneScope.Foo() { - Box(Modifier.testTag("fooParentIn${sceneKey.debugName}")) { + fun ContentScope.Foo() { + Box(Modifier.testTag("fooParentIn${contentKey.debugName}")) { Box(Modifier.element(TestElements.Foo).size(20.dp)) } } @@ -1973,7 +1973,7 @@ class ElementTest { val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) } @Composable - fun SceneScope.Foo(offset: Dp) { + fun ContentScope.Foo(offset: Dp) { Box(Modifier.fillMaxSize()) { Box(Modifier.offset(offset, offset).element(TestElements.Foo).size(20.dp)) } @@ -2041,7 +2041,7 @@ class ElementTest { } @Composable - fun SceneScope.Foo() { + fun ContentScope.Foo() { Box(Modifier.element(TestElements.Foo).size(10.dp)) } @@ -2062,7 +2062,11 @@ class ElementTest { rule.waitForIdle() assertThat( - layoutImpl.elements.getValue(TestElements.Foo).sceneStates.getValue(SceneB).lastSize + layoutImpl.elements + .getValue(TestElements.Foo) + .stateByContent + .getValue(SceneB) + .lastSize ) .isEqualTo(Element.SizeUnspecified) } @@ -2078,8 +2082,8 @@ class ElementTest { // In A => B, Foo is not shared and first fades out from A then fades in // B. sharedElement(TestElements.Foo, enabled = false) - fractionRange(end = 0.5f) { fade(TestElements.Foo.inScene(SceneA)) } - fractionRange(start = 0.5f) { fade(TestElements.Foo.inScene(SceneB)) } + fractionRange(end = 0.5f) { fade(TestElements.Foo.inContent(SceneA)) } + fractionRange(start = 0.5f) { fade(TestElements.Foo.inContent(SceneB)) } } from(SceneB, to = SceneA) { @@ -2091,7 +2095,7 @@ class ElementTest { } @Composable - fun SceneScope.Foo(modifier: Modifier = Modifier) { + fun ContentScope.Foo(modifier: Modifier = Modifier) { Box(modifier.element(TestElements.Foo).size(10.dp)) } @@ -2149,7 +2153,7 @@ class ElementTest { val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) } @Composable - fun SceneScope.Foo(modifier: Modifier = Modifier) { + fun ContentScope.Foo(modifier: Modifier = Modifier) { Box(modifier.element(TestElements.Foo).size(10.dp)) } @@ -2216,7 +2220,7 @@ class ElementTest { // verify that preview transition for exiting elements is halfway played from // current-scene-value -> preview-target-value - val exiting1InB = layoutImpl.elements.getValue(exiting1).sceneStates.getValue(SceneB) + val exiting1InB = layoutImpl.elements.getValue(exiting1).stateByContent.getValue(SceneB) // e.g. exiting1 is half scaled... assertThat(exiting1InB.lastScale).isEqualTo(Scale(0.9f, 0.9f, Offset.Unspecified)) // ...and exiting2 is halfway translated from 0.dp to 20.dp... @@ -2228,7 +2232,7 @@ class ElementTest { // verify that preview transition for entering elements is halfway played from // preview-target-value -> transition-target-value (or target-scene-value if no // transition-target-value defined). - val entering1InA = layoutImpl.elements.getValue(entering1).sceneStates.getValue(SceneA) + val entering1InA = layoutImpl.elements.getValue(entering1).stateByContent.getValue(SceneA) // e.g. entering1 is half scaled between 0f and 0.5f -> 0.25f... assertThat(entering1InA.lastScale).isEqualTo(Scale(0.25f, 0.25f, Offset.Unspecified)) // ...and entering2 is half way translated between 30.dp and 0.dp @@ -2272,7 +2276,7 @@ class ElementTest { // verify that exiting elements remain in the preview-end state if no further transition is // defined for them in the second stage - val exiting1InB = layoutImpl.elements.getValue(exiting1).sceneStates.getValue(SceneB) + val exiting1InB = layoutImpl.elements.getValue(exiting1).stateByContent.getValue(SceneB) // i.e. exiting1 remains half scaled assertThat(exiting1InB.lastScale).isEqualTo(Scale(0.9f, 0.9f, Offset.Unspecified)) // in case there is an additional transition defined for the second stage, verify that the @@ -2286,7 +2290,7 @@ class ElementTest { rule.onNode(isElement(exiting3)).assertSizeIsEqualTo(90.dp, 90.dp) // verify that entering elements animate seamlessly to their target state - val entering1InA = layoutImpl.elements.getValue(entering1).sceneStates.getValue(SceneA) + val entering1InA = layoutImpl.elements.getValue(entering1).stateByContent.getValue(SceneA) // e.g. entering1, which was scaled from 0f to 0.25f during the preview phase, should now be // half way scaled between 0.25f and its target-state of 1f -> 0.625f assertThat(entering1InA.lastScale).isEqualTo(Scale(0.625f, 0.625f, Offset.Unspecified)) @@ -2318,7 +2322,7 @@ class ElementTest { } @Composable - fun SceneScope.Foo(elementKey: ElementKey) { + fun ContentScope.Foo(elementKey: ElementKey) { Box(Modifier.element(elementKey).size(100.dp)) } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt index 9523896e5c00..821cc2927443 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt @@ -62,7 +62,7 @@ class MovableElementTest { } @Composable - private fun SceneScope.MovableCounter(key: ElementKey, modifier: Modifier) { + private fun ContentScope.MovableCounter(key: ElementKey, modifier: Modifier) { MovableElement(key, modifier) { content { Counter() } } } @@ -264,7 +264,7 @@ class MovableElementTest { @Test fun movableElementContentIsRecomposedIfContentParametersChange() { @Composable - fun SceneScope.MovableFoo(text: String, modifier: Modifier = Modifier) { + fun ContentScope.MovableFoo(text: String, modifier: Modifier = Modifier) { MovableElement(TestElements.Foo, modifier) { content { Text(text) } } } @@ -298,7 +298,7 @@ class MovableElementTest { @Test fun elementScopeExtendsBoxScope() { rule.setContent { - TestSceneScope { + TestContentScope { Element(TestElements.Foo, Modifier.size(200.dp)) { content { Box(Modifier.testTag("bottomEnd").align(Alignment.BottomEnd)) @@ -315,7 +315,7 @@ class MovableElementTest { @Test fun movableElementScopeExtendsBoxScope() { rule.setContent { - TestSceneScope { + TestContentScope { MovableElement(TestElements.Foo, Modifier.size(200.dp)) { content { Box(Modifier.testTag("bottomEnd").align(Alignment.BottomEnd)) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt index 311a58018840..9ebc42650d45 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt @@ -48,7 +48,7 @@ class NestedScrollToSceneTest { private val layoutHeight = 400.dp private fun setup2ScenesAndScrollTouchSlop( - modifierSceneA: @Composable SceneScope.() -> Modifier = { Modifier }, + modifierSceneA: @Composable ContentScope.() -> Modifier = { Modifier }, ): MutableSceneTransitionLayoutState { val state = rule.runOnUiThread { diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt index 1ec10793363e..32f3bac5054e 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt @@ -129,7 +129,7 @@ class SceneTransitionLayoutTest { } @Composable - private fun SceneScope.SharedFoo(size: Dp, childOffset: Dp, modifier: Modifier = Modifier) { + private fun ContentScope.SharedFoo(size: Dp, childOffset: Dp, modifier: Modifier = Modifier) { Element(TestElements.Foo, modifier.size(size).background(Color.Red)) { // Offset the single child of Foo by some animated shared offset. val offset by animateElementDpAsState(childOffset, TestValues.Value1) @@ -479,14 +479,14 @@ class SceneTransitionLayoutTest { fun sceneKeyInScope() { val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } - var keyInA: SceneKey? = null - var keyInB: SceneKey? = null - var keyInC: SceneKey? = null + var keyInA: ContentKey? = null + var keyInB: ContentKey? = null + var keyInC: ContentKey? = null rule.setContent { SceneTransitionLayout(state) { - scene(SceneA) { keyInA = sceneKey } - scene(SceneB) { keyInB = sceneKey } - scene(SceneC) { keyInC = sceneKey } + scene(SceneA) { keyInA = contentKey } + scene(SceneB) { keyInB = contentKey } + scene(SceneC) { keyInC = contentKey } } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt index 6233608dfad0..c9f71da1691b 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt @@ -25,7 +25,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.android.compose.animation.scene.SceneScope +import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.TestElements import com.android.compose.animation.scene.TransitionBuilder import com.android.compose.animation.scene.TransitionRecordingSpec @@ -108,8 +108,8 @@ class AnchoredSizeTest { } private fun assertBarSizeMatchesGolden( - fromSceneContent: @Composable SceneScope.() -> Unit, - toSceneContent: @Composable SceneScope.() -> Unit, + fromSceneContent: @Composable ContentScope.() -> Unit, + toSceneContent: @Composable ContentScope.() -> Unit, transition: TransitionBuilder.() -> Unit, ) { val recordingSpec = diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt index 8001f418a12c..00acb137a833 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt @@ -31,7 +31,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.TestElements import com.android.compose.animation.scene.TestScenes -import com.android.compose.animation.scene.inScene +import com.android.compose.animation.scene.inContent import com.android.compose.animation.scene.testTransition import com.android.compose.test.assertSizeIsEqualTo import org.junit.Rule @@ -125,10 +125,10 @@ class SharedElementTest { sharedElement(TestElements.Foo, enabled = false) // In SceneA, Foo leaves to the left edge. - translate(TestElements.Foo.inScene(TestScenes.SceneA), Edge.Left) + translate(TestElements.Foo.inContent(TestScenes.SceneA), Edge.Left) // In SceneB, Foo comes from the bottom edge. - translate(TestElements.Foo.inScene(TestScenes.SceneB), Edge.Bottom) + translate(TestElements.Foo.inContent(TestScenes.SceneB), Edge.Bottom) }, ) { before { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp) } diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestSceneScope.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt index fbd557f3cbb3..00adefb150c1 100644 --- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestSceneScope.kt +++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt @@ -20,11 +20,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -/** `SceneScope` for tests, which allows a single scene to be drawn in a [SceneTransitionLayout]. */ +/** + * [ContentScope] for tests, which allows a single scene to be drawn in a [SceneTransitionLayout]. + */ @Composable -fun TestSceneScope( +fun TestContentScope( modifier: Modifier = Modifier, - content: @Composable SceneScope.() -> Unit, + content: @Composable ContentScope.() -> Unit, ) { val currentScene = remember { SceneKey("current") } val state = remember { MutableSceneTransitionLayoutState(currentScene) } diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt index a37d78ef8a71..7f26b9855a9a 100644 --- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt +++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt @@ -18,9 +18,7 @@ package com.android.compose.animation.scene import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.SemanticsNode import androidx.compose.ui.test.SemanticsNodeInteraction @@ -87,8 +85,8 @@ interface TransitionTestAssertionScope { * @sample com.android.compose.animation.scene.transformation.TranslateTest */ fun ComposeContentTestRule.testTransition( - fromSceneContent: @Composable SceneScope.() -> Unit, - toSceneContent: @Composable SceneScope.() -> Unit, + fromSceneContent: @Composable ContentScope.() -> Unit, + toSceneContent: @Composable ContentScope.() -> Unit, transition: TransitionBuilder.() -> Unit, layoutModifier: Modifier = Modifier, fromScene: SceneKey = TestScenes.SceneA, @@ -134,8 +132,8 @@ fun TimeSeriesCaptureScope<SemanticsNodeInteractionsProvider>.featureOfElement( /** Records the transition between two scenes of [transitionLayout][SceneTransitionLayout]. */ fun MotionTestRule<ComposeToolkit>.recordTransition( - fromSceneContent: @Composable SceneScope.() -> Unit, - toSceneContent: @Composable SceneScope.() -> Unit, + fromSceneContent: @Composable ContentScope.() -> Unit, + toSceneContent: @Composable ContentScope.() -> Unit, transition: TransitionBuilder.() -> Unit, recordingSpec: TransitionRecordingSpec, layoutModifier: Modifier = Modifier, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt index 6ad4b317b94c..314631823e96 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt @@ -675,6 +675,24 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() { assertThat(tiles!!.size).isEqualTo(3) } + @Test + fun changeInPackagesTiles_doesntTriggerUserChange_logged() = + testScope.runTest(USER_INFO_0) { + val specs = + listOf( + TileSpec.create("a"), + ) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + runCurrent() + // Settled on the same list of tiles. + assertThat(underTest.currentTilesSpecs).isEqualTo(specs) + + installedTilesPackageRepository.setInstalledPackagesForUser(USER_INFO_0.id, emptySet()) + runCurrent() + + verify(logger, never()).logTileUserChanged(TileSpec.create("a"), 0) + } + private fun QSTile.State.fillIn(state: Int, label: CharSequence, secondaryLabel: CharSequence) { this.state = state this.label = label diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index d7c3527bf9a2..ba37d589a4f8 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3681,7 +3681,10 @@ <!-- Touchpad back gesture action name in tutorial [CHAR LIMIT=NONE] --> <string name="touchpad_back_gesture_action_title">Go back</string> <!-- Touchpad back gesture guidance in gestures tutorial [CHAR LIMIT=NONE] --> - <string name="touchpad_back_gesture_guidance">To go back, swipe left or right using three fingers anywhere on the touchpad.</string> + <string name="touchpad_back_gesture_guidance">To go back, swipe left or right using three fingers anywhere on the touchpad.\n\nYou can also use the keyboard shortcut +Action + ESC for this.</string> + <!-- Text shown to the user after they complete back gesture tutorial [CHAR LIMIT=NONE] --> + <string name="touchpad_back_gesture_finished">You completed the go back gesture.</string> <string name="touchpad_back_gesture_animation_content_description">Touchpad showing three fingers moving right and left</string> <string name="touchpad_back_gesture_screen_animation_content_description">Device screen showing animation for back gesture</string> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 7f5839d4f1fb..0da252da5cc9 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -2086,6 +2086,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private void handleUserUnlocked(int userId) { Assert.isMainThread(); + mLogger.logUserUnlocked(userId); mUserIsUnlocked.put(userId, true); mNeedsSlowUnlockTransition = resolveNeedsSlowUnlockTransition(); for (int i = 0; i < mCallbacks.size(); i++) { @@ -2098,12 +2099,15 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private void handleUserStopped(int userId) { Assert.isMainThread(); - mUserIsUnlocked.put(userId, mUserManager.isUserUnlocked(userId)); + boolean isUnlocked = mUserManager.isUserUnlocked(userId); + mLogger.logUserStopped(userId, isUnlocked); + mUserIsUnlocked.put(userId, isUnlocked); } @VisibleForTesting void handleUserRemoved(int userId) { Assert.isMainThread(); + mLogger.logUserRemoved(userId); mUserIsUnlocked.delete(userId); mUserTrustIsUsuallyManaged.delete(userId); } @@ -2444,7 +2448,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mTaskStackChangeListeners.registerTaskStackListener(mTaskStackListener); int user = mSelectedUserInteractor.getSelectedUserId(true); - mUserIsUnlocked.put(user, mUserManager.isUserUnlocked(user)); + boolean isUserUnlocked = mUserManager.isUserUnlocked(user); + mLogger.logUserUnlockedInitialState(user, isUserUnlocked); + mUserIsUnlocked.put(user, isUserUnlocked); mLogoutEnabled = mDevicePolicyManager.isLogoutEnabled(); updateSecondaryLockscreenRequirement(user); List<UserInfo> allUsers = mUserManager.getUsers(); @@ -4059,6 +4065,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab pw.println(" strongAuthFlags=" + Integer.toHexString(strongAuthFlags)); pw.println("ActiveUnlockRunning=" + mTrustManager.isActiveUnlockRunning(mSelectedUserInteractor.getSelectedUserId())); + pw.println("userUnlockedCache[userid=" + userId + "]=" + isUserUnlocked(userId)); + pw.println("actualUserUnlocked[userid=" + userId + "]=" + + mUserManager.isUserUnlocked(userId)); new DumpsysTableLogger( "KeyguardActiveUnlockTriggers", KeyguardActiveUnlockModel.TABLE_HEADERS, diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt index 1f4e732c21e4..0b58f06e0d5d 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt @@ -391,6 +391,7 @@ constructor(@KeyguardUpdateMonitorLog private val logBuffer: LogBuffer) { { "handleTimeFormatUpdate timeFormat=$str1" } ) } + fun logUdfpsPointerDown(sensorId: Int) { logBuffer.log(TAG, DEBUG, { int1 = sensorId }, { "onUdfpsPointerDown, sensorId: $int1" }) } @@ -639,12 +640,45 @@ constructor(@KeyguardUpdateMonitorLog private val logBuffer: LogBuffer) { { "fingerprint acquire message: $int1" } ) } + fun logForceIsDismissibleKeyguard(keepUnlocked: Boolean) { logBuffer.log( - TAG, - DEBUG, - { bool1 = keepUnlocked }, - { "keepUnlockedOnFold changed to: $bool1" } + TAG, + DEBUG, + { bool1 = keepUnlocked }, + { "keepUnlockedOnFold changed to: $bool1" } + ) + } + + fun logUserUnlocked(userId: Int) { + logBuffer.log(TAG, DEBUG, { int1 = userId }, { "userUnlocked userId: $int1" }) + } + + fun logUserStopped(userId: Int, isUnlocked: Boolean) { + logBuffer.log( + TAG, + DEBUG, + { + int1 = userId + bool1 = isUnlocked + }, + { "userStopped userId: $int1 isUnlocked: $bool1" } + ) + } + + fun logUserRemoved(userId: Int) { + logBuffer.log(TAG, DEBUG, { int1 = userId }, { "userRemoved userId: $int1" }) + } + + fun logUserUnlockedInitialState(userId: Int, isUnlocked: Boolean) { + logBuffer.log( + TAG, + DEBUG, + { + int1 = userId + bool1 = isUnlocked + }, + { "userUnlockedInitialState userId: $int1 isUnlocked: $bool1" } ) } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java index f041f4d5963f..083f1db07886 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java @@ -52,6 +52,7 @@ import androidx.annotation.VisibleForTesting; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import com.android.internal.logging.UiEventLogger; import com.android.settingslib.bluetooth.BluetoothCallback; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.HapClientProfile; @@ -104,6 +105,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, private final AudioManager mAudioManager; private final LocalBluetoothProfileManager mProfileManager; private final HapClientProfile mHapClientProfile; + private final UiEventLogger mUiEventLogger; private HearingDevicesListAdapter mDeviceListAdapter; private HearingDevicesPresetsController mPresetsController; private Context mApplicationContext; @@ -163,7 +165,8 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, DialogTransitionAnimator dialogTransitionAnimator, @Nullable LocalBluetoothManager localBluetoothManager, @Main Handler handler, - AudioManager audioManager) { + AudioManager audioManager, + UiEventLogger uiEventLogger) { mApplicationContext = applicationContext; mShowPairNewDevice = showPairNewDevice; mSystemUIDialogFactory = systemUIDialogFactory; @@ -174,6 +177,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, mAudioManager = audioManager; mProfileManager = localBluetoothManager.getProfileManager(); mHapClientProfile = mProfileManager.getHapClientProfile(); + mUiEventLogger = uiEventLogger; } @Override @@ -187,6 +191,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, @Override public void onDeviceItemGearClicked(@NonNull DeviceItem deviceItem, @NonNull View view) { + mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_GEAR_CLICK); dismissDialogIfExists(); Intent intent = new Intent(ACTION_BLUETOOTH_DEVICE_DETAILS); Bundle bundle = new Bundle(); @@ -198,13 +203,21 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, } @Override - public void onDeviceItemOnClicked(@NonNull DeviceItem deviceItem, @NonNull View view) { + public void onDeviceItemClicked(@NonNull DeviceItem deviceItem, @NonNull View view) { CachedBluetoothDevice cachedBluetoothDevice = deviceItem.getCachedBluetoothDevice(); switch (deviceItem.getType()) { - case ACTIVE_MEDIA_BLUETOOTH_DEVICE, CONNECTED_BLUETOOTH_DEVICE -> - cachedBluetoothDevice.disconnect(); - case AVAILABLE_MEDIA_BLUETOOTH_DEVICE -> cachedBluetoothDevice.setActive(); - case SAVED_BLUETOOTH_DEVICE -> cachedBluetoothDevice.connect(); + case ACTIVE_MEDIA_BLUETOOTH_DEVICE, CONNECTED_BLUETOOTH_DEVICE -> { + mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_DISCONNECT); + cachedBluetoothDevice.disconnect(); + } + case AVAILABLE_MEDIA_BLUETOOTH_DEVICE -> { + mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_SET_ACTIVE); + cachedBluetoothDevice.setActive(); + } + case SAVED_BLUETOOTH_DEVICE -> { + mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_CONNECT); + cachedBluetoothDevice.connect(); + } } } @@ -262,6 +275,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, if (mLocalBluetoothManager == null) { return; } + mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_DIALOG_SHOW); mPairButton = dialog.requireViewById(R.id.pair_new_device_button); mDeviceList = dialog.requireViewById(R.id.device_list); mPresetSpinner = dialog.requireViewById(R.id.preset_spinner); @@ -341,12 +355,17 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, } }); + // Refresh the spinner and setSelection(index, false) before setOnItemSelectedListener() to + // avoid extra onItemSelected() get called when first register the listener. + final List<BluetoothHapPresetInfo> presetInfos = mPresetsController.getAllPresetInfo(); + final int activePresetIndex = mPresetsController.getActivePresetIndex(); + refreshPresetInfoAdapter(presetInfos, activePresetIndex); mPresetSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { + mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_PRESET_SELECT); mPresetsController.selectPreset( mPresetsController.getAllPresetInfo().get(position).getIndex()); - mPresetSpinner.setSelection(position); } @Override @@ -354,9 +373,6 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, // Do nothing } }); - final List<BluetoothHapPresetInfo> presetInfos = mPresetsController.getAllPresetInfo(); - final int activePresetIndex = mPresetsController.getActivePresetIndex(); - refreshPresetInfoAdapter(presetInfos, activePresetIndex); mPresetSpinner.setVisibility( (activeHearingDevice != null && activeHearingDevice.isConnectedHapClientDevice() && !mPresetInfoAdapter.isEmpty()) ? VISIBLE : GONE); @@ -365,6 +381,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, private void setupPairNewDeviceButton(SystemUIDialog dialog, @Visibility int visibility) { if (visibility == VISIBLE) { mPairButton.setOnClickListener(v -> { + mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_PAIR); dismissDialogIfExists(); final Intent intent = new Intent(Settings.ACTION_HEARING_DEVICE_PAIRING_SETTINGS); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); @@ -413,7 +430,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, final int size = mPresetInfoAdapter.getCount(); for (int position = 0; position < size; position++) { if (presetInfos.get(position).getIndex() == activePresetIndex) { - mPresetSpinner.setSelection(position); + mPresetSpinner.setSelection(position, /* animate= */ false); } } } @@ -464,12 +481,15 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, text.setText(item.getToolName()); Intent intent = item.getToolIntent(); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - view.setOnClickListener( - v -> { - dismissDialogIfExists(); - mActivityStarter.postStartActivityDismissingKeyguard(intent, /* delay= */ 0, - mDialogTransitionAnimator.createActivityTransitionController(view)); - }); + view.setOnClickListener(v -> { + final String name = intent.getComponent() != null + ? intent.getComponent().flattenToString() + : intent.getPackage() + "/" + intent.getAction(); + mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_RELATED_TOOL_CLICK, 0, name); + dismissDialogIfExists(); + mActivityStarter.postStartActivityDismissingKeyguard(intent, /* delay= */ 0, + mDialogTransitionAnimator.createActivityTransitionController(view)); + }); return view; } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java index 737805b4d62f..b46b8fe4f6c8 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java @@ -96,7 +96,7 @@ public class HearingDevicesListAdapter extends RecyclerView.Adapter<RecyclerView * @param deviceItem bluetooth device item * @param view the view that was clicked */ - void onDeviceItemOnClicked(@NonNull DeviceItem deviceItem, @NonNull View view); + void onDeviceItemClicked(@NonNull DeviceItem deviceItem, @NonNull View view); } private static class DeviceItemViewHolder extends RecyclerView.ViewHolder { @@ -119,7 +119,7 @@ public class HearingDevicesListAdapter extends RecyclerView.Adapter<RecyclerView public void bindView(DeviceItem item, HearingDeviceItemCallback callback) { mContainer.setEnabled(item.isEnabled()); - mContainer.setOnClickListener(view -> callback.onDeviceItemOnClicked(item, view)); + mContainer.setOnClickListener(view -> callback.onDeviceItemClicked(item, view)); Integer backgroundResId = item.getBackground(); if (backgroundResId != null) { mContainer.setBackground(mContext.getDrawable(item.getBackground())); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesUiEvent.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesUiEvent.java new file mode 100644 index 000000000000..3fbe56eccef2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesUiEvent.java @@ -0,0 +1,51 @@ +/* + * 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.accessibility.hearingaid; + +import com.android.internal.logging.UiEvent; +import com.android.internal.logging.UiEventLogger; + +public enum HearingDevicesUiEvent implements UiEventLogger.UiEventEnum { + + @UiEvent(doc = "Hearing devices dialog is shown") + HEARING_DEVICES_DIALOG_SHOW(1848), + @UiEvent(doc = "Pair new device") + HEARING_DEVICES_PAIR(1849), + @UiEvent(doc = "Connect to the device") + HEARING_DEVICES_CONNECT(1850), + @UiEvent(doc = "Disconnect from the device") + HEARING_DEVICES_DISCONNECT(1851), + @UiEvent(doc = "Set the device as active device") + HEARING_DEVICES_SET_ACTIVE(1852), + @UiEvent(doc = "Click on the device gear to enter device detail page") + HEARING_DEVICES_GEAR_CLICK(1853), + @UiEvent(doc = "Select a preset from preset spinner") + HEARING_DEVICES_PRESET_SELECT(1854), + @UiEvent(doc = "Click on related tool") + HEARING_DEVICES_RELATED_TOOL_CLICK(1856); + + private final int mId; + + HearingDevicesUiEvent(int id) { + mId = id; + } + + @Override + public int getId() { + return mId; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 88601dab891d..42866465a0cc 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -30,6 +30,7 @@ import com.android.systemui.dreams.AssistantAttentionMonitor import com.android.systemui.dreams.DreamMonitor import com.android.systemui.dreams.homecontrols.HomeControlsDreamStartable import com.android.systemui.globalactions.GlobalActionsComponent +import com.android.systemui.inputdevice.oobe.KeyboardTouchpadOobeTutorialCoreStartable import com.android.systemui.keyboard.KeyboardUI import com.android.systemui.keyboard.PhysicalKeyboardCoreStartable import com.android.systemui.keyguard.KeyguardViewConfigurator @@ -257,6 +258,13 @@ abstract class SystemUICoreStartableModule { @Binds @IntoMap + @ClassKey(KeyboardTouchpadOobeTutorialCoreStartable::class) + abstract fun bindOobeSchedulerCoreStartable( + listener: KeyboardTouchpadOobeTutorialCoreStartable + ): CoreStartable + + @Binds + @IntoMap @ClassKey(PhysicalKeyboardCoreStartable::class) abstract fun bindKeyboardCoreStartable(listener: PhysicalKeyboardCoreStartable): CoreStartable diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 15ddf5bfd281..a44807248d23 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -144,6 +144,7 @@ import com.android.systemui.statusbar.ui.binder.StatusBarViewBinderModule; import com.android.systemui.statusbar.window.StatusBarWindowModule; import com.android.systemui.telephony.data.repository.TelephonyRepositoryModule; import com.android.systemui.temporarydisplay.dagger.TemporaryDisplayModule; +import com.android.systemui.touchpad.TouchpadModule; import com.android.systemui.tuner.dagger.TunerModule; import com.android.systemui.user.UserModule; import com.android.systemui.user.domain.UserDomainLayerModule; @@ -259,6 +260,7 @@ import javax.inject.Named; CommonSystemUIUnfoldModule.class, TelephonyRepositoryModule.class, TemporaryDisplayModule.class, + TouchpadModule.class, TunerModule.class, UserDomainLayerModule.class, UserModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/doze/AlwaysOnDisplayPolicy.java b/packages/SystemUI/src/com/android/systemui/doze/AlwaysOnDisplayPolicy.java index e182d0b224fc..f80e0bece7cb 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/AlwaysOnDisplayPolicy.java +++ b/packages/SystemUI/src/com/android/systemui/doze/AlwaysOnDisplayPolicy.java @@ -57,21 +57,29 @@ public class AlwaysOnDisplayPolicy { /** - * Integer used to dim the screen while dozing. + * Integer in the scale [1, 255] used to dim the screen while dozing. * * @see R.integer.config_screenBrightnessDoze */ public int defaultDozeBrightness; /** - * Integer used to dim the screen just before the screen turns off. + * Integer in the scale [1, 255] used to dim the screen just before the screen turns off. * * @see R.integer.config_screenBrightnessDim */ public int dimBrightness; /** - * Integer array to map ambient brightness type to real screen brightness. + * Float in the scale [0, 1] used to dim the screen just before the screen turns off. + * + * @see R.integer.config_screenBrightnessDimFloat + */ + public float dimBrightnessFloat; + + /** + * Integer array to map ambient brightness type to real screen brightness in the integer scale + * [1, 255]. * * @see Settings.Global#ALWAYS_ON_DISPLAY_CONSTANTS * @see #KEY_SCREEN_BRIGHTNESS_ARRAY @@ -189,6 +197,8 @@ public class AlwaysOnDisplayPolicy { com.android.internal.R.integer.config_screenBrightnessDoze); dimBrightness = resources.getInteger( com.android.internal.R.integer.config_screenBrightnessDim); + dimBrightnessFloat = resources.getFloat( + com.android.internal.R.dimen.config_screenBrightnessDimFloat); screenBrightnessArray = mParser.getIntArray(KEY_SCREEN_BRIGHTNESS_ARRAY, resources.getIntArray( R.array.config_doze_brightness_sensor_to_brightness)); diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeBrightnessHostForwarder.java b/packages/SystemUI/src/com/android/systemui/doze/DozeBrightnessHostForwarder.java index cf0dcad5bf0d..0b336143c34a 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeBrightnessHostForwarder.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeBrightnessHostForwarder.java @@ -36,4 +36,10 @@ public class DozeBrightnessHostForwarder extends DozeMachine.Service.Delegate { super.setDozeScreenBrightness(brightness); mHost.setDozeScreenBrightness(brightness); } + + @Override + public void setDozeScreenBrightnessFloat(float brightness) { + super.setDozeScreenBrightnessFloat(brightness); + mHost.setDozeScreenBrightnessFloat(brightness); + } } diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java b/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java index 17b455d7ef6f..2e7a459b0e9b 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java @@ -71,11 +71,17 @@ public interface DozeHost { /** * Sets the actual display brightness. - * @param value from 0 to 255. + * @param value from 1 to 255. */ void setDozeScreenBrightness(int value); /** + * Sets the actual display brightness. + * @param value from {@link PowerManager#BRIGHTNESS_MIN} to {@link PowerManager#BRIGHTNESS_MAX}. + */ + void setDozeScreenBrightnessFloat(float value); + + /** * Fade out screen before switching off the display power mode. * @param onDisplayOffCallback Executed when the display is black. */ diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java index 9a9e698e0138..5bfcc975d02d 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java @@ -401,13 +401,22 @@ public class DozeLog implements Dumpable { /** * Appends new AOD screen brightness to logs - * @param brightness display brightness setting + * @param brightness display brightness setting between 1 and 255 */ public void traceDozeScreenBrightness(int brightness) { mLogger.logDozeScreenBrightness(brightness); } /** + * Appends new AOD screen brightness to logs + * @param brightness display brightness setting between {@link PowerManager#BRIGHTNESS_MIN} and + * {@link PowerManager#BRIGHTNESS_MAX} + */ + public void traceDozeScreenBrightnessFloat(float brightness) { + mLogger.logDozeScreenBrightnessFloat(brightness); + } + + /** * Appends new AOD dimming scrim opacity to logs * @param scrimOpacity */ diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt index 9d6693efffa3..a31dbec242fe 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt @@ -309,7 +309,15 @@ class DozeLogger @Inject constructor( buffer.log(TAG, INFO, { int1 = brightness }, { - "Doze screen brightness set, brightness=$int1" + "Doze screen brightness set (int), brightness=$int1" + }) + } + + fun logDozeScreenBrightnessFloat(brightness: Float) { + buffer.log(TAG, INFO, { + double1 = brightness.toDouble() + }, { + "Doze screen brightness set (float), brightness=$double1" }) } diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java index 7f0b16bca8b4..8198ef41bb49 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java @@ -507,9 +507,13 @@ public class DozeMachine { /** Request waking up. */ void requestWakeUp(@DozeLog.Reason int reason); - /** Set screen brightness */ + /** Set screen brightness between 1 and 255 */ void setDozeScreenBrightness(int brightness); + /** Set screen brightness between {@link PowerManager#BRIGHTNESS_MIN} and + * {@link PowerManager#BRIGHTNESS_MAX} */ + void setDozeScreenBrightnessFloat(float brightness); + class Delegate implements Service { private final Service mDelegate; private final Executor mBgExecutor; @@ -540,6 +544,13 @@ public class DozeMachine { mDelegate.setDozeScreenBrightness(brightness); }); } + + @Override + public void setDozeScreenBrightnessFloat(float brightness) { + mBgExecutor.execute(() -> { + mDelegate.setDozeScreenBrightnessFloat(brightness); + }); + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java index 323ed9871dc3..6ed84e573d7c 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java @@ -20,6 +20,8 @@ import static android.os.PowerManager.GO_TO_SLEEP_REASON_TIMEOUT; import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_GOING_TO_SLEEP; +import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -27,6 +29,7 @@ import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; +import android.hardware.display.DisplayManager; import android.os.Handler; import android.os.PowerManager; import android.os.SystemProperties; @@ -34,6 +37,7 @@ import android.os.Trace; import android.os.UserHandle; import android.provider.Settings; import android.util.IndentingPrintWriter; +import android.view.Display; import com.android.internal.R; import com.android.systemui.doze.dagger.BrightnessSensor; @@ -46,6 +50,7 @@ import com.android.systemui.util.sensors.AsyncSensorManager; import com.android.systemui.util.settings.SystemSettings; import java.io.PrintWriter; +import java.util.Arrays; import java.util.Objects; import java.util.Optional; @@ -74,6 +79,7 @@ public class DozeScreenBrightness extends BroadcastReceiver implements DozeMachi private final DozeHost mDozeHost; private final Handler mHandler; private final SensorManager mSensorManager; + private final DisplayManager mDisplayManager; private final Optional<Sensor>[] mLightSensorOptional; // light sensors to use per posture private final WakefulnessLifecycle mWakefulnessLifecycle; private final DozeParameters mDozeParameters; @@ -81,13 +87,17 @@ public class DozeScreenBrightness extends BroadcastReceiver implements DozeMachi private final DozeLog mDozeLog; private final SystemSettings mSystemSettings; private final int[] mSensorToBrightness; + @Nullable + private final float[] mSensorToBrightnessFloat; private final int[] mSensorToScrimOpacity; private final int mScreenBrightnessDim; + private final float mScreenBrightnessDimFloat; @DevicePostureController.DevicePostureInt private int mDevicePosture; private boolean mRegistered; private int mDefaultDozeBrightness; + private float mDefaultDozeBrightnessFloat; private boolean mPaused = false; private boolean mScreenOff = false; private int mLastSensorValue = -1; @@ -102,6 +112,7 @@ public class DozeScreenBrightness extends BroadcastReceiver implements DozeMachi private int mDebugBrightnessBucket = -1; @Inject + @SuppressLint("AndroidFrameworkRequiresPermission") public DozeScreenBrightness( Context context, @WrappedService DozeMachine.Service service, @@ -113,10 +124,12 @@ public class DozeScreenBrightness extends BroadcastReceiver implements DozeMachi DozeParameters dozeParameters, DevicePostureController devicePostureController, DozeLog dozeLog, - SystemSettings systemSettings) { + SystemSettings systemSettings, + DisplayManager displayManager) { mContext = context; mDozeService = service; mSensorManager = sensorManager; + mDisplayManager = displayManager; mLightSensorOptional = lightSensorOptional; mDevicePostureController = devicePostureController; mDevicePosture = mDevicePostureController.getDevicePosture(); @@ -131,8 +144,13 @@ public class DozeScreenBrightness extends BroadcastReceiver implements DozeMachi R.dimen.config_screenBrightnessMinimumDimAmountFloat); mDefaultDozeBrightness = alwaysOnDisplayPolicy.defaultDozeBrightness; + mDefaultDozeBrightnessFloat = + mDisplayManager.getDefaultDozeBrightness(mContext.getDisplayId()); mScreenBrightnessDim = alwaysOnDisplayPolicy.dimBrightness; + mScreenBrightnessDimFloat = alwaysOnDisplayPolicy.dimBrightnessFloat; mSensorToBrightness = alwaysOnDisplayPolicy.screenBrightnessArray; + mSensorToBrightnessFloat = + mDisplayManager.getDozeBrightnessSensorValueToBrightness(mContext.getDisplayId()); mSensorToScrimOpacity = alwaysOnDisplayPolicy.dimmingScrimArray; mDevicePostureController.addCallback(mDevicePostureCallback); @@ -193,11 +211,22 @@ public class DozeScreenBrightness extends BroadcastReceiver implements DozeMachi if (force || mRegistered || mDebugBrightnessBucket != -1) { int sensorValue = mDebugBrightnessBucket == -1 ? mLastSensorValue : mDebugBrightnessBucket; - int brightness = computeBrightness(sensorValue); - boolean brightnessReady = brightness > 0; - if (brightnessReady) { - mDozeService.setDozeScreenBrightness( - clampToDimBrightnessForScreenOff(clampToUserSetting(brightness))); + boolean brightnessReady; + if (shouldUseFloatBrightness()) { + float brightness = computeBrightnessFloat(sensorValue); + brightnessReady = brightness >= 0; + if (brightnessReady) { + mDozeService.setDozeScreenBrightnessFloat( + clampToDimBrightnessForScreenOffFloat( + clampToUserSettingFloat(brightness))); + } + } else { + int brightness = computeBrightness(sensorValue); + brightnessReady = brightness > 0; + if (brightnessReady) { + mDozeService.setDozeScreenBrightness( + clampToDimBrightnessForScreenOff(clampToUserSetting(brightness))); + } } int scrimOpacity = -1; @@ -249,17 +278,30 @@ public class DozeScreenBrightness extends BroadcastReceiver implements DozeMachi return mSensorToBrightness[sensorValue]; } + private float computeBrightnessFloat(int sensorValue) { + if (sensorValue < 0 || sensorValue >= mSensorToBrightnessFloat.length) { + return -1; + } + return mSensorToBrightnessFloat[sensorValue]; + } + @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { } private void resetBrightnessToDefault() { - mDozeService.setDozeScreenBrightness( - clampToDimBrightnessForScreenOff( - clampToUserSetting(mDefaultDozeBrightness))); + if (shouldUseFloatBrightness()) { + mDozeService.setDozeScreenBrightnessFloat( + clampToDimBrightnessForScreenOffFloat( + clampToUserSettingFloat(mDefaultDozeBrightnessFloat))); + } else { + mDozeService.setDozeScreenBrightness( + clampToDimBrightnessForScreenOff( + clampToUserSetting(mDefaultDozeBrightness))); + } mDozeHost.setAodDimmingScrim(0f); } - //TODO: brightnessfloat change usages to float. + private int clampToUserSetting(int brightness) { int screenBrightnessModeSetting = mSystemSettings.getIntForUser( Settings.System.SCREEN_BRIGHTNESS_MODE, @@ -274,6 +316,19 @@ public class DozeScreenBrightness extends BroadcastReceiver implements DozeMachi return Math.min(brightness, userSetting); } + @SuppressLint("AndroidFrameworkRequiresPermission") + private float clampToUserSettingFloat(float brightness) { + int screenBrightnessModeSetting = mSystemSettings.getIntForUser( + Settings.System.SCREEN_BRIGHTNESS_MODE, + Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL, UserHandle.USER_CURRENT); + if (screenBrightnessModeSetting == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC) { + return brightness; + } + + float userSetting = mDisplayManager.getBrightness(Display.DEFAULT_DISPLAY); + return Math.min(brightness, userSetting); + } + /** * Clamp the brightness to the dim brightness value used by PowerManagerService just before the * device times out and goes to sleep, if we are sleeping from a timeout. This ensures that we @@ -301,6 +356,31 @@ public class DozeScreenBrightness extends BroadcastReceiver implements DozeMachi } } + /** + * Clamp the brightness to the dim brightness value used by PowerManagerService just before the + * device times out and goes to sleep, if we are sleeping from a timeout. This ensures that we + * don't raise the brightness back to the user setting before or during the screen off + * animation. + */ + private float clampToDimBrightnessForScreenOffFloat(float brightness) { + final boolean screenTurningOff = + (mDozeParameters.shouldClampToDimBrightness() + || mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_GOING_TO_SLEEP) + && mState == DozeMachine.State.INITIALIZED; + if (screenTurningOff + && mWakefulnessLifecycle.getLastSleepReason() == GO_TO_SLEEP_REASON_TIMEOUT) { + return Math.max( + PowerManager.BRIGHTNESS_MIN, + // Use the lower of either the dim brightness, or the current brightness reduced + // by the minimum dim amount. This is the same logic used in + // DisplayPowerController#updatePowerState to apply a minimum dim amount. + Math.min(brightness - mScreenBrightnessMinimumDimAmountFloat, + mScreenBrightnessDimFloat)); + } else { + return brightness; + } + } + private void setLightSensorEnabled(boolean enabled) { if (enabled && !mRegistered && isLightSensorPresent()) { // Wait until we get an event from the sensor until indicating ready. @@ -342,6 +422,20 @@ public class DozeScreenBrightness extends BroadcastReceiver implements DozeMachi idpw.increaseIndent(); idpw.println("registered=" + mRegistered); idpw.println("posture=" + DevicePostureController.devicePostureToString(mDevicePosture)); + idpw.println("sensorToBrightness=" + Arrays.toString(mSensorToBrightness)); + idpw.println("sensorToBrightnessFloat=" + Arrays.toString(mSensorToBrightnessFloat)); + idpw.println("sensorToScrimOpacity=" + Arrays.toString(mSensorToScrimOpacity)); + idpw.println("screenBrightnessDim=" + mScreenBrightnessDim); + idpw.println("screenBrightnessDimFloat=" + mScreenBrightnessDimFloat); + idpw.println("mDefaultDozeBrightness=" + mDefaultDozeBrightness); + idpw.println("mDefaultDozeBrightnessFloat=" + mDefaultDozeBrightnessFloat); + idpw.println("mLastSensorValue=" + mLastSensorValue); + idpw.println("shouldUseFloatBrightness()=" + shouldUseFloatBrightness()); + } + + private boolean shouldUseFloatBrightness() { + return com.android.server.display.feature.flags.Flags.dozeBrightnessFloat() + && mSensorToBrightnessFloat != null; } private final DevicePostureController.Callback mDevicePostureCallback = diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt index 3b161b659af9..5a008bddc748 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt @@ -45,7 +45,7 @@ constructor( data class DeviceAdded(val deviceId: Int) : DeviceChange - data object DeviceRemoved : DeviceChange + data class DeviceRemoved(val deviceId: Int) : DeviceChange data object FreshStart : DeviceChange @@ -72,7 +72,7 @@ constructor( override fun onInputDeviceRemoved(deviceId: Int) { connectedDevices = connectedDevices - deviceId - sendWithLogging(connectedDevices to DeviceRemoved) + sendWithLogging(connectedDevices to DeviceRemoved(deviceId)) } } sendWithLogging(connectedDevices to FreshStart) diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/KeyboardTouchpadOobeTutorialCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/KeyboardTouchpadOobeTutorialCoreStartable.kt new file mode 100644 index 000000000000..dbfea7688e0d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/KeyboardTouchpadOobeTutorialCoreStartable.kt @@ -0,0 +1,37 @@ +/* + * 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.inputdevice.oobe + +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.inputdevice.oobe.domain.interactor.OobeTutorialSchedulerInteractor +import com.android.systemui.shared.Flags.newTouchpadGesturesTutorial +import dagger.Lazy +import javax.inject.Inject + +/** A [CoreStartable] to launch a scheduler for keyboard and touchpad OOBE education */ +@SysUISingleton +class KeyboardTouchpadOobeTutorialCoreStartable +@Inject +constructor(private val oobeTutorialSchedulerInteractor: Lazy<OobeTutorialSchedulerInteractor>) : + CoreStartable { + override fun start() { + if (newTouchpadGesturesTutorial()) { + oobeTutorialSchedulerInteractor.get().start() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/domain/interactor/OobeTutorialSchedulerInteractor.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/domain/interactor/OobeTutorialSchedulerInteractor.kt new file mode 100644 index 000000000000..0d69081adfa4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/domain/interactor/OobeTutorialSchedulerInteractor.kt @@ -0,0 +1,58 @@ +/* + * 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.inputdevice.oobe.domain.interactor + +import android.content.Context +import android.content.Intent +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyboard.data.repository.KeyboardRepository +import com.android.systemui.touchpad.data.repository.TouchpadRepository +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** When keyboards or touchpads are connected, schedule a tutorial after given time has elapsed */ +@SysUISingleton +class OobeTutorialSchedulerInteractor +@Inject +constructor( + @Application private val context: Context, + @Application private val applicationScope: CoroutineScope, + keyboardRepository: KeyboardRepository, + touchpadRepository: TouchpadRepository +) { + private val isAnyKeyboardConnected = keyboardRepository.isAnyKeyboardConnected + private val isAnyTouchpadConnected = touchpadRepository.isAnyTouchpadConnected + + fun start() { + applicationScope.launch { isAnyKeyboardConnected.collect { startOobe() } } + applicationScope.launch { isAnyTouchpadConnected.collect { startOobe() } } + } + + private fun startOobe() { + val intent = Intent(TUTORIAL_ACTION) + intent.addCategory(Intent.CATEGORY_DEFAULT) + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + } + + companion object { + const val TAG = "OobeSchedulerInteractor" + const val TUTORIAL_ACTION = "com.android.systemui.action.TOUCHPAD_TUTORIAL" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt index 817849c41297..b6543074cdef 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt @@ -41,6 +41,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn @@ -78,9 +79,15 @@ constructor( ) : KeyboardRepository { private val keyboardsChange: Flow<Pair<Collection<Int>, DeviceChange>> = - inputDeviceRepository.deviceChange.map { (ids, change) -> - ids.filter { id -> isPhysicalFullKeyboard(id) } to change - } + inputDeviceRepository.deviceChange + .map { (ids, change) -> ids.filter { id -> isPhysicalFullKeyboard(id) } to change } + .filter { (_, change) -> + when (change) { + FreshStart -> true + is DeviceAdded -> isPhysicalFullKeyboard(change.deviceId) + is DeviceRemoved -> isPhysicalFullKeyboard(change.deviceId) + } + } @FlowPreview override val newlyConnectedKeyboard: Flow<Keyboard> = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt index e2d7851daf7e..04ea37e70139 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt @@ -112,14 +112,18 @@ constructor( keyguardInteractor.isActiveDreamLockscreenHosted, communalSceneInteractor.isIdleOnCommunal ) - .filterRelevantKeyguardState() - .collect { - (isBouncerShowing, isAwake, isActiveDreamLockscreenHosted, isIdleOnCommunal) - -> + .filterRelevantKeyguardStateAnd { (isBouncerShowing, _, _, _) -> + // TODO(b/307976454) - See if we need to listen for SHOW_WHEN_LOCKED + // activities showing up over the bouncer. Camera launch can't show up over + // bouncer since the first power press hides bouncer. Do occluding + // activities auto hide bouncer? Not sure. + !isBouncerShowing + } + .collect { (_, isAwake, isActiveDreamLockscreenHosted, isIdleOnCommunal) -> if ( !maybeStartTransitionToOccludedOrInsecureCamera { state, reason -> startTransitionTo(state, ownerReason = reason) - } && !isBouncerShowing && isAwake && !isActiveDreamLockscreenHosted + } && isAwake && !isActiveDreamLockscreenHosted ) { val toState = if (isIdleOnCommunal) { @@ -254,6 +258,6 @@ constructor( val TO_GONE_SHORT_DURATION = 200.milliseconds val TO_LOCKSCREEN_DURATION = 450.milliseconds val TO_GLANCEABLE_HUB_DURATION = DEFAULT_DURATION - val TO_GONE_SURFACE_BEHIND_VISIBLE_THRESHOLD = 0.5f + val TO_GONE_SURFACE_BEHIND_VISIBLE_THRESHOLD = 0.1f } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt index 805dbb08d1ac..2ebd9e8c5f2b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt @@ -30,6 +30,7 @@ import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisionin import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -52,11 +53,12 @@ constructor( * then we'll seed the repository with a transition from OFF -> GONE. */ @OptIn(ExperimentalCoroutinesApi::class) - private val showLockscreenOnBoot = + private val showLockscreenOnBoot: Flow<Boolean> by lazy { deviceProvisioningInteractor.isDeviceProvisioned.map { provisioned -> (provisioned || deviceEntryInteractor.isAuthenticationRequired()) && deviceEntryInteractor.isLockscreenEnabled() } + } override fun start() { scope.launch { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt index 4bfefda63466..3fffeffec254 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt @@ -17,7 +17,7 @@ package com.android.systemui.keyguard.ui.viewmodel import android.content.res.Resources -import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.ContentKey import com.android.internal.annotations.VisibleForTesting import com.android.systemui.biometrics.AuthController import com.android.systemui.dagger.SysUISingleton @@ -90,12 +90,12 @@ constructor( /** * Returns a flow that indicates whether lockscreen notifications should be rendered in the - * given [sceneKey]. + * given [contentKey]. */ - fun areNotificationsVisible(sceneKey: SceneKey): Flow<Boolean> { + fun areNotificationsVisible(contentKey: ContentKey): Flow<Boolean> { // `Scenes.NotificationsShade` renders its own separate notifications stack, so when it's // open we avoid rendering the lockscreen notifications stack. - if (sceneKey == Scenes.NotificationsShade) { + if (contentKey == Scenes.NotificationsShade) { return flowOf(false) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt index 97b5e87d7167..02379e6efecc 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt @@ -46,7 +46,7 @@ import com.android.systemui.qs.toProto import com.android.systemui.retail.data.repository.RetailModeRepository import com.android.systemui.settings.UserTracker import com.android.systemui.user.data.repository.UserRepository -import com.android.systemui.util.kotlin.pairwise +import com.android.systemui.util.kotlin.pairwiseBy import dagger.Lazy import java.io.PrintWriter import javax.inject.Inject @@ -63,7 +63,6 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -169,17 +168,19 @@ constructor( private val userAndTiles = currentUser .flatMapLatest { userId -> - tileSpecRepository.tilesSpecs(userId).map { UserAndTiles(userId, it) } + val currentTiles = tileSpecRepository.tilesSpecs(userId) + val installedComponents = + installedTilesComponentRepository.getInstalledTilesComponents(userId) + currentTiles.combine(installedComponents) { tiles, components -> + UserTilesAndComponents(userId, tiles, components) + } } .distinctUntilChanged() - .pairwise(UserAndTiles(-1, emptyList())) + .pairwiseBy(UserTilesAndComponents(-1, emptyList(), emptySet())) { prev, new -> + DataWithUserChange(data = new, userChange = prev.userId != new.userId) + } .flowOn(backgroundDispatcher) - private val installedPackagesWithTiles = - currentUser.flatMapLatest { - installedTilesComponentRepository.getInstalledTilesComponents(it) - } - private val minTiles: Int get() = if (retailModeRepository.inRetailMode) { @@ -194,7 +195,6 @@ constructor( } } - @OptIn(ExperimentalCoroutinesApi::class) private fun startTileCollection() { scope.launch { launch { @@ -205,95 +205,82 @@ constructor( } launch(backgroundDispatcher) { - userAndTiles - .combine(installedPackagesWithTiles) { usersAndTiles, packages -> - Data( - usersAndTiles.previousValue, - usersAndTiles.newValue, - packages, - ) - } - .collectLatest { - val newTileList = it.newData.tiles - val userChanged = it.oldData.userId != it.newData.userId - val newUser = it.newData.userId - val components = it.installedComponents - - // Destroy all tiles that are not in the new set - specsToTiles - .filter { - it.key !in newTileList && it.value is TileOrNotInstalled.Tile - } - .forEach { entry -> - logger.logTileDestroyed( - entry.key, - if (userChanged) { - QSPipelineLogger.TileDestroyedReason - .TILE_NOT_PRESENT_IN_NEW_USER - } else { - QSPipelineLogger.TileDestroyedReason.TILE_REMOVED - } - ) - (entry.value as TileOrNotInstalled.Tile).tile.destroy() - } - // MutableMap will keep the insertion order - val newTileMap = mutableMapOf<TileSpec, TileOrNotInstalled>() - - newTileList.forEach { tileSpec -> - if (tileSpec !in newTileMap) { - if ( - tileSpec is TileSpec.CustomTileSpec && - tileSpec.componentName !in components - ) { - newTileMap[tileSpec] = TileOrNotInstalled.NotInstalled + userAndTiles.collectLatest { + val newUser = it.userId + val newTileList = it.tiles + val components = it.installedComponents + val userChanged = it.userChange + + // Destroy all tiles that are not in the new set + specsToTiles + .filter { it.key !in newTileList && it.value is TileOrNotInstalled.Tile } + .forEach { entry -> + logger.logTileDestroyed( + entry.key, + if (userChanged) { + QSPipelineLogger.TileDestroyedReason + .TILE_NOT_PRESENT_IN_NEW_USER } else { - // Create tile here will never try to create a CustomTile that - // is not installed - val newTile = - if (tileSpec in specsToTiles) { - processExistingTile( - tileSpec, - specsToTiles.getValue(tileSpec), - userChanged, - newUser - ) - ?: createTile(tileSpec) - } else { - createTile(tileSpec) - } - if (newTile != null) { - newTileMap[tileSpec] = TileOrNotInstalled.Tile(newTile) + QSPipelineLogger.TileDestroyedReason.TILE_REMOVED + } + ) + (entry.value as TileOrNotInstalled.Tile).tile.destroy() + } + // MutableMap will keep the insertion order + val newTileMap = mutableMapOf<TileSpec, TileOrNotInstalled>() + + newTileList.forEach { tileSpec -> + if (tileSpec !in newTileMap) { + if ( + tileSpec is TileSpec.CustomTileSpec && + tileSpec.componentName !in components + ) { + newTileMap[tileSpec] = TileOrNotInstalled.NotInstalled + } else { + // Create tile here will never try to create a CustomTile that + // is not installed + val newTile = + if (tileSpec in specsToTiles) { + processExistingTile( + tileSpec, + specsToTiles.getValue(tileSpec), + userChanged, + newUser + ) ?: createTile(tileSpec) + } else { + createTile(tileSpec) } + if (newTile != null) { + newTileMap[tileSpec] = TileOrNotInstalled.Tile(newTile) } } } + } - val resolvedSpecs = newTileMap.keys.toList() - specsToTiles.clear() - specsToTiles.putAll(newTileMap) - val newResolvedTiles = - newTileMap - .filter { it.value is TileOrNotInstalled.Tile } - .map { - TileModel(it.key, (it.value as TileOrNotInstalled.Tile).tile) - } - - _currentSpecsAndTiles.value = newResolvedTiles - logger.logTilesNotInstalled( - newTileMap.filter { it.value is TileOrNotInstalled.NotInstalled }.keys, - newUser - ) - if (newResolvedTiles.size < minTiles) { - // We ended up with not enough tiles (some may be not installed). - // Prepend the default set of tiles - launch { tileSpecRepository.prependDefault(currentUser.value) } - } else if (resolvedSpecs != newTileList) { - // There were some tiles that couldn't be created. Change the value in - // the - // repository - launch { tileSpecRepository.setTiles(currentUser.value, resolvedSpecs) } - } + val resolvedSpecs = newTileMap.keys.toList() + specsToTiles.clear() + specsToTiles.putAll(newTileMap) + val newResolvedTiles = + newTileMap + .filter { it.value is TileOrNotInstalled.Tile } + .map { TileModel(it.key, (it.value as TileOrNotInstalled.Tile).tile) } + + _currentSpecsAndTiles.value = newResolvedTiles + logger.logTilesNotInstalled( + newTileMap.filter { it.value is TileOrNotInstalled.NotInstalled }.keys, + newUser + ) + if (newResolvedTiles.size < minTiles) { + // We ended up with not enough tiles (some may be not installed). + // Prepend the default set of tiles + launch { tileSpecRepository.prependDefault(currentUser.value) } + } else if (resolvedSpecs != newTileList) { + // There were some tiles that couldn't be created. Change the value in + // the + // repository + launch { tileSpecRepository.setTiles(currentUser.value, resolvedSpecs) } } + } } } } @@ -362,8 +349,7 @@ constructor( newQSTileFactory.get().createTile(spec.spec) } else { null - } - ?: tileFactory.createTile(spec.spec) + } ?: tileFactory.createTile(spec.spec) } if (tile == null) { logger.logTileNotFoundInFactory(spec) @@ -436,15 +422,25 @@ constructor( @JvmInline value class Tile(val tile: QSTile) : TileOrNotInstalled } +} - private data class UserAndTiles( - val userId: Int, - val tiles: List<TileSpec>, - ) - - private data class Data( - val oldData: UserAndTiles, - val newData: UserAndTiles, - val installedComponents: Set<ComponentName>, +private data class UserTilesAndComponents( + val userId: Int, + val tiles: List<TileSpec>, + val installedComponents: Set<ComponentName> +) + +private data class DataWithUserChange( + val userId: Int, + val tiles: List<TileSpec>, + val installedComponents: Set<ComponentName>, + val userChange: Boolean, +) + +private fun DataWithUserChange(data: UserTilesAndComponents, userChange: Boolean) = + DataWithUserChange( + data.userId, + data.tiles, + data.installedComponents, + userChange, ) -} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java index d9546ec6ac51..1750347fd2ae 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java @@ -106,7 +106,8 @@ public class WorkModeTile extends QSTileImpl<BooleanState> implements @Override @MainThread public void onManagedProfileRemoved() { - mHost.removeTile(getTileSpec()); + // No OP as this may race with the user change in CurrentTilesInteractor. + // If the tile needs to be removed, AutoAdd (or AutoTileManager) will take care of that. } @Override diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt index e9e9d8b0bbfc..cdcefdb50b0f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt @@ -22,12 +22,15 @@ import androidx.annotation.StringRes import com.android.internal.logging.InstanceId import com.android.systemui.qs.pipeline.shared.TileSpec -data class QSTileConfig( +data class QSTileConfig +@JvmOverloads +constructor( val tileSpec: TileSpec, val uiConfig: QSTileUIConfig, val instanceId: InstanceId, val metricsSpec: String = tileSpec.spec, val policy: QSTilePolicy = QSTilePolicy.NoRestrictions, + val autoRemoveOnUnavailable: Boolean = true, ) /** @@ -38,6 +41,7 @@ sealed interface QSTileUIConfig { val iconRes: Int @DrawableRes get + val labelRes: Int @StringRes get @@ -48,6 +52,7 @@ sealed interface QSTileUIConfig { data object Empty : QSTileUIConfig { override val iconRes: Int get() = Resources.ID_NULL + override val labelRes: Int get() = Resources.ID_NULL } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt index 2cdcc24d149f..c6f9ae8f4463 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt @@ -70,7 +70,7 @@ constructor( applicationScope.launch { launch { qsTileViewModel.isAvailable.collectIndexed { index, isAvailable -> - if (!isAvailable) { + if (!isAvailable && qsTileViewModel.config.autoRemoveOnUnavailable) { qsHost.removeTile(tileSpec) } // qsTileViewModel.isAvailable flow often starts with isAvailable == true. diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java index b60c193d63b0..7db521ef3057 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java @@ -341,6 +341,11 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW mScreenBrightnessDoze = value / 255f; } + @Override + public void setDozeScreenBrightnessFloat(float value) { + mScreenBrightnessDoze = value; + } + private void setKeyguardDark(boolean dark) { int vis = mWindowRootView.getSystemUiVisibility(); if (dark) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java index 707d59aa560d..85fad420daf1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java @@ -131,10 +131,20 @@ public interface NotificationShadeWindowController extends RemoteInputController /** Sets the state of whether the remote input is active or not. */ default void onRemoteInputActive(boolean remoteInputActive) {} - /** Sets the screen brightness level for when the device is dozing. */ + /** + * Sets the screen brightness level for when the device is dozing. + * @param value The brightness value between 1 and 255 + */ default void setDozeScreenBrightness(int value) {} /** + * Sets the screen brightness level for when the device is dozing. + * @param value The brightness value between {@link PowerManager#BRIGHTNESS_MIN} and + * {@link PowerManager#BRIGHTNESS_MAX} + */ + default void setDozeScreenBrightnessFloat(float value) {} + + /** * Sets whether the screen brightness is forced to the value we use for doze mode by the status * bar window. No-op if the device does not support dozing. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt index ac2a0d898081..1e0e597ad3e8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt @@ -20,10 +20,14 @@ import android.app.Notification import android.os.UserHandle import com.android.keyguard.KeyguardUpdateMonitor import com.android.server.notification.Flags.screenshareNotificationHiding +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.StatusBarState -import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter import com.android.systemui.statusbar.notification.DynamicPrivacyController import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.ListEntry @@ -32,27 +36,33 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController import com.android.systemui.user.domain.interactor.SelectedUserInteractor import dagger.Binds import dagger.Module import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.launch @Module(includes = [PrivateSensitiveContentCoordinatorModule::class]) interface SensitiveContentCoordinatorModule @Module interface PrivateSensitiveContentCoordinatorModule { - @Binds - fun bindCoordinator(impl: SensitiveContentCoordinatorImpl): SensitiveContentCoordinator + @Binds fun bindCoordinator(impl: SensitiveContentCoordinatorImpl): SensitiveContentCoordinator } /** Coordinates re-inflation and post-processing of sensitive notification content. */ interface SensitiveContentCoordinator : Coordinator @CoordinatorScope -class SensitiveContentCoordinatorImpl @Inject constructor( +class SensitiveContentCoordinatorImpl +@Inject +constructor( private val dynamicPrivacyController: DynamicPrivacyController, private val lockscreenUserManager: NotificationLockscreenUserManager, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, @@ -61,45 +71,85 @@ class SensitiveContentCoordinatorImpl @Inject constructor( private val selectedUserInteractor: SelectedUserInteractor, private val sensitiveNotificationProtectionController: SensitiveNotificationProtectionController, -) : Invalidator("SensitiveContentInvalidator"), - SensitiveContentCoordinator, - DynamicPrivacyController.Listener, - OnBeforeRenderListListener { - private val onSensitiveStateChanged = Runnable() { - invalidateList("onSensitiveStateChanged") - } - - private val screenshareSecretFilter = object : NotifFilter("ScreenshareSecretFilter") { - val NotificationEntry.isSecret - get() = channel?.lockscreenVisibility == Notification.VISIBILITY_SECRET || - sbn.notification?.visibility == Notification.VISIBILITY_SECRET - override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean { - return screenshareNotificationHiding() && - sensitiveNotificationProtectionController.isSensitiveStateActive && - entry.isSecret + private val deviceEntryInteractor: DeviceEntryInteractor, + private val sceneInteractor: SceneInteractor, + @Application private val scope: CoroutineScope, +) : + Invalidator("SensitiveContentInvalidator"), + SensitiveContentCoordinator, + DynamicPrivacyController.Listener, + OnBeforeRenderListListener { + private var inTransitionFromLockedToGone = false + + private val onSensitiveStateChanged = Runnable() { invalidateList("onSensitiveStateChanged") } + + private val screenshareSecretFilter = + object : NotifFilter("ScreenshareSecretFilter") { + val NotificationEntry.isSecret + get() = + channel?.lockscreenVisibility == Notification.VISIBILITY_SECRET || + sbn.notification?.visibility == Notification.VISIBILITY_SECRET + + override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean { + return screenshareNotificationHiding() && + sensitiveNotificationProtectionController.isSensitiveStateActive && + entry.isSecret + } } - } override fun attach(pipeline: NotifPipeline) { dynamicPrivacyController.addListener(this) if (screenshareNotificationHiding()) { - sensitiveNotificationProtectionController - .registerSensitiveStateListener(onSensitiveStateChanged) + sensitiveNotificationProtectionController.registerSensitiveStateListener( + onSensitiveStateChanged + ) } pipeline.addOnBeforeRenderListListener(this) pipeline.addPreRenderInvalidator(this) if (screenshareNotificationHiding()) { pipeline.addFinalizeFilter(screenshareSecretFilter) } + + if (SceneContainerFlag.isEnabled) { + scope.launch { + sceneInteractor.transitionState + .mapNotNull { + val transitioningToGone = it.isTransitioning(to = Scenes.Gone) + val deviceEntered = deviceEntryInteractor.isDeviceEntered.value + when { + transitioningToGone && !deviceEntered -> true + !transitioningToGone -> false + else -> null + } + } + .distinctUntilChanged() + .collect { + inTransitionFromLockedToGone = it + invalidateList("inTransitionFromLockedToGoneChanged") + } + } + } } override fun onDynamicPrivacyChanged(): Unit = invalidateList("onDynamicPrivacyChanged") + private val isKeyguardGoingAway: Boolean + get() { + if (SceneContainerFlag.isEnabled) { + return inTransitionFromLockedToGone + } else { + return keyguardStateController.isKeyguardGoingAway + } + } + override fun onBeforeRenderList(entries: List<ListEntry>) { - if (keyguardStateController.isKeyguardGoingAway || + if ( + isKeyguardGoingAway || statusBarStateController.state == StatusBarState.KEYGUARD && - keyguardUpdateMonitor.getUserUnlockedWithBiometricAndIsBypassing( - selectedUserInteractor.getSelectedUserId())) { + keyguardUpdateMonitor.getUserUnlockedWithBiometricAndIsBypassing( + selectedUserInteractor.getSelectedUserId() + ) + ) { // don't update yet if: // - the keyguard is currently going away // - LS is about to be dismissed by a biometric that bypasses LS (avoid notif flash) @@ -109,35 +159,40 @@ class SensitiveContentCoordinatorImpl @Inject constructor( return } - val isSensitiveContentProtectionActive = screenshareNotificationHiding() && - sensitiveNotificationProtectionController.isSensitiveStateActive + val isSensitiveContentProtectionActive = + screenshareNotificationHiding() && + sensitiveNotificationProtectionController.isSensitiveStateActive val currentUserId = lockscreenUserManager.currentUserId val devicePublic = lockscreenUserManager.isLockscreenPublicMode(currentUserId) - val deviceSensitive = (devicePublic && + val deviceSensitive = + (devicePublic && !lockscreenUserManager.userAllowsPrivateNotificationsInPublic(currentUserId)) || isSensitiveContentProtectionActive val dynamicallyUnlocked = dynamicPrivacyController.isDynamicallyUnlocked for (entry in extractAllRepresentativeEntries(entries).filter { it.rowExists() }) { val notifUserId = entry.sbn.user.identifier - val userLockscreen = devicePublic || - lockscreenUserManager.isLockscreenPublicMode(notifUserId) - val userPublic = when { - // if we're not on the lockscreen, we're definitely private - !userLockscreen -> false - // we are on the lockscreen, so unless we're dynamically unlocked, we're - // definitely public - !dynamicallyUnlocked -> true - // we're dynamically unlocked, but check if the notification needs - // a separate challenge if it's from a work profile - else -> when (notifUserId) { - currentUserId -> false - UserHandle.USER_ALL -> false - else -> lockscreenUserManager.needsSeparateWorkChallenge(notifUserId) + val userLockscreen = + devicePublic || lockscreenUserManager.isLockscreenPublicMode(notifUserId) + val userPublic = + when { + // if we're not on the lockscreen, we're definitely private + !userLockscreen -> false + // we are on the lockscreen, so unless we're dynamically unlocked, we're + // definitely public + !dynamicallyUnlocked -> true + // we're dynamically unlocked, but check if the notification needs + // a separate challenge if it's from a work profile + else -> + when (notifUserId) { + currentUserId -> false + UserHandle.USER_ALL -> false + else -> lockscreenUserManager.needsSeparateWorkChallenge(notifUserId) + } } - } - val shouldProtectNotification = screenshareNotificationHiding() && - sensitiveNotificationProtectionController.shouldProtectNotification(entry) + val shouldProtectNotification = + screenshareNotificationHiding() && + sensitiveNotificationProtectionController.shouldProtectNotification(entry) val needsRedaction = lockscreenUserManager.needsRedaction(entry) val isSensitive = userPublic && needsRedaction @@ -149,9 +204,7 @@ class SensitiveContentCoordinatorImpl @Inject constructor( } } -private fun extractAllRepresentativeEntries( - entries: List<ListEntry> -): Sequence<NotificationEntry> = +private fun extractAllRepresentativeEntries(entries: List<ListEntry>): Sequence<NotificationEntry> = entries.asSequence().flatMap(::extractAllRepresentativeEntries) private fun extractAllRepresentativeEntries(listEntry: ListEntry): Sequence<NotificationEntry> = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt index 2a8db56bd18c..a6ca3ab8bce3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt @@ -98,7 +98,8 @@ class PeekDisabledSuppressor( globalSettings.registerContentObserverSync( globalSettings.getUriFor(HEADS_UP_NOTIFICATIONS_ENABLED), /* notifyForDescendants = */ true, - observer) + observer + ) // QQQ: Do we need to register for SETTING_HEADS_UP_TICKER? It seems unused. @@ -147,12 +148,12 @@ class PeekAlreadyBubbledSuppressor( } } -class PeekDndSuppressor() : +class PeekDndSuppressor : VisualInterruptionFilter(types = setOf(PEEK), reason = "suppressed by DND") { override fun shouldSuppress(entry: NotificationEntry) = entry.shouldSuppressPeek() } -class PeekNotImportantSuppressor() : +class PeekNotImportantSuppressor : VisualInterruptionFilter(types = setOf(PEEK), reason = "importance < HIGH") { override fun shouldSuppress(entry: NotificationEntry) = entry.importance < IMPORTANCE_HIGH } @@ -170,7 +171,10 @@ class PeekDeviceNotInUseSuppressor( class PeekOldWhenSuppressor(private val systemClock: SystemClock) : VisualInterruptionFilter( - types = setOf(PEEK), reason = "has old `when`", uiEventId = HUN_SUPPRESSED_OLD_WHEN) { + types = setOf(PEEK), + reason = "has old `when`", + uiEventId = HUN_SUPPRESSED_OLD_WHEN + ) { private fun whenAge(entry: NotificationEntry) = systemClock.currentTimeMillis() - entry.sbn.notification.getWhen() @@ -190,47 +194,51 @@ class PeekOldWhenSuppressor(private val systemClock: SystemClock) : } } -class PulseEffectSuppressor() : +class PulseEffectSuppressor : VisualInterruptionFilter(types = setOf(PULSE), reason = "suppressed by DND") { override fun shouldSuppress(entry: NotificationEntry) = entry.shouldSuppressAmbient() } -class PulseLockscreenVisibilityPrivateSuppressor() : +class PulseLockscreenVisibilityPrivateSuppressor : VisualInterruptionFilter( - types = setOf(PULSE), reason = "hidden by lockscreen visibility override") { + types = setOf(PULSE), + reason = "hidden by lockscreen visibility override" + ) { override fun shouldSuppress(entry: NotificationEntry) = entry.ranking.lockscreenVisibilityOverride == VISIBILITY_PRIVATE } -class PulseLowImportanceSuppressor() : +class PulseLowImportanceSuppressor : VisualInterruptionFilter(types = setOf(PULSE), reason = "importance < DEFAULT") { override fun shouldSuppress(entry: NotificationEntry) = entry.importance < IMPORTANCE_DEFAULT } -class HunGroupAlertBehaviorSuppressor() : +class HunGroupAlertBehaviorSuppressor : VisualInterruptionFilter( - types = setOf(PEEK, PULSE), reason = "suppressive group alert behavior") { + types = setOf(PEEK, PULSE), + reason = "suppressive group alert behavior" + ) { override fun shouldSuppress(entry: NotificationEntry) = entry.sbn.let { it.isGroup && it.notification.suppressAlertingDueToGrouping() } } -class HunSilentNotificationSuppressor() : +class HunSilentNotificationSuppressor : VisualInterruptionFilter(types = setOf(PEEK, PULSE), reason = "notification isSilent") { override fun shouldSuppress(entry: NotificationEntry) = entry.sbn.let { Flags.notificationSilentFlag() && it.notification.isSilent } } -class HunJustLaunchedFsiSuppressor() : +class HunJustLaunchedFsiSuppressor : VisualInterruptionFilter(types = setOf(PEEK, PULSE), reason = "just launched FSI") { override fun shouldSuppress(entry: NotificationEntry) = entry.hasJustLaunchedFullScreenIntent() } -class BubbleNotAllowedSuppressor() : - VisualInterruptionFilter(types = setOf(BUBBLE), reason = "cannot bubble") { +class BubbleNotAllowedSuppressor : + VisualInterruptionFilter(types = setOf(BUBBLE), reason = "cannot bubble", isSpammy = true) { override fun shouldSuppress(entry: NotificationEntry) = !entry.canBubble() } -class BubbleNoMetadataSuppressor() : +class BubbleNoMetadataSuppressor : VisualInterruptionFilter(types = setOf(BUBBLE), reason = "has no or invalid bubble metadata") { private fun isValidMetadata(metadata: BubbleMetadata?) = @@ -253,6 +261,7 @@ class AlertKeyguardVisibilitySuppressor( /** * Set with: + * * adb shell setprop persist.force_show_avalanche_edu_once 1 && adb shell stop; adb shell start */ private const val FORCE_SHOW_AVALANCHE_EDU_ONCE = "persist.force_show_avalanche_edu_once" @@ -368,7 +377,8 @@ class AvalancheSuppressor( val bundle = Bundle() bundle.putString( Notification.EXTRA_SUBSTITUTE_APP_NAME, - context.getString(com.android.internal.R.string.android_system_label)) + context.getString(com.android.internal.R.string.android_system_label) + ) val builder = Notification.Builder(context, NotificationChannels.ALERTS) @@ -390,8 +400,10 @@ class AvalancheSuppressor( } private fun calculateState(entry: NotificationEntry): State { - if (entry.ranking.isConversation && - entry.sbn.notification.getWhen() > avalancheProvider.startTime) { + if ( + entry.ranking.isConversation && + entry.sbn.notification.getWhen() > avalancheProvider.startTime + ) { uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_NEW_CONVERSATION) return State.ALLOW_CONVERSATION_AFTER_AVALANCHE } @@ -424,8 +436,10 @@ class AvalancheSuppressor( uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_COLORIZED) return State.ALLOW_COLORIZED } - if (packageManager.checkPermission(RECEIVE_EMERGENCY_BROADCAST, entry.sbn.packageName) == - PERMISSION_GRANTED) { + if ( + packageManager.checkPermission(RECEIVE_EMERGENCY_BROADCAST, entry.sbn.packageName) == + PERMISSION_GRANTED + ) { uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_EMERGENCY) return State.ALLOW_EMERGENCY } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt index 1470b0331359..c204ea9097de 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.interruption +import android.util.Log import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel.DEBUG import com.android.systemui.log.core.LogLevel.INFO @@ -24,11 +25,15 @@ import com.android.systemui.log.dagger.NotificationInterruptLog import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider.FullScreenIntentDecision import com.android.systemui.statusbar.notification.logKey +import com.android.systemui.util.Compile import javax.inject.Inject class VisualInterruptionDecisionLogger @Inject constructor(@NotificationInterruptLog val buffer: LogBuffer) { + + val spew: Boolean = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE) + fun logHeadsUpFeatureChanged(isEnabled: Boolean) { buffer.log( TAG, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt index c0d27cb1ad83..8e8d9b69ac58 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt @@ -95,7 +95,8 @@ constructor( private constructor( val decision: DecisionImpl, override val uiEventId: UiEventEnum? = null, - override val eventLogData: EventLogData? = null + override val eventLogData: EventLogData? = null, + val isSpammy: Boolean = false, ) : Loggable { companion object { val unsuppressed = @@ -113,7 +114,8 @@ constructor( LoggableDecision( DecisionImpl(shouldInterrupt = false, logReason = suppressor.reason), uiEventId = suppressor.uiEventId, - eventLogData = suppressor.eventLogData + eventLogData = suppressor.eventLogData, + isSpammy = suppressor.isSpammy, ) } } @@ -185,8 +187,15 @@ constructor( if (NotificationAvalancheSuppression.isEnabled) { addFilter( - AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, - packageManager, uiEventLogger, context, notificationManager) + AvalancheSuppressor( + avalancheProvider, + systemClock, + settingsInteractor, + packageManager, + uiEventLogger, + context, + notificationManager + ) ) avalancheProvider.register() } @@ -280,7 +289,9 @@ constructor( entry: NotificationEntry, loggableDecision: LoggableDecision ) { - logger.logDecision(type.name, entry, loggableDecision.decision) + if (!loggableDecision.isSpammy || logger.spew) { + logger.logDecision(type.name, entry, loggableDecision.decision) + } logEvents(entry, loggableDecision) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt index ee797274deac..5fe75c0cb3f9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt @@ -59,6 +59,10 @@ sealed interface VisualInterruptionSuppressor { /** Optional data to be logged in the EventLog when this suppresses an interruption. */ val eventLogData: EventLogData? + /** Whether the interruption is spammy and should be dropped under normal circumstances. */ + val isSpammy: Boolean + get() = false + /** * Called after the suppressor is added to the [VisualInterruptionDecisionProvider] but before * any other methods are called on the suppressor. @@ -76,7 +80,7 @@ abstract class VisualInterruptionCondition( constructor( types: Set<VisualInterruptionType>, reason: String - ) : this(types, reason, /* uiEventId = */ null) + ) : this(types, reason, /* uiEventId= */ null) /** @return true if these interruptions should be suppressed right now. */ abstract fun shouldSuppress(): Boolean @@ -87,12 +91,13 @@ abstract class VisualInterruptionFilter( override val types: Set<VisualInterruptionType>, override val reason: String, override val uiEventId: UiEventEnum? = null, - override val eventLogData: EventLogData? = null + override val eventLogData: EventLogData? = null, + override val isSpammy: Boolean = false, ) : VisualInterruptionSuppressor { constructor( types: Set<VisualInterruptionType>, reason: String - ) : this(types, reason, /* uiEventId = */ null) + ) : this(types, reason, /* uiEventId= */ null) /** * @param entry the notification to consider suppressing diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 461a38df797f..b6de78e70994 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -2995,7 +2995,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { @Override public void onFalse() { // Hides quick settings, bouncer, and quick-quick settings. - mStatusBarKeyguardViewManager.reset(true); + mStatusBarKeyguardViewManager.reset(true, /* isFalsingReset= */true); } }; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java index a32d5fef58eb..ca1fb78bdb42 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java @@ -437,6 +437,13 @@ public final class DozeServiceHost implements DozeHost { mNotificationShadeWindowController.setDozeScreenBrightness(brightness); } + + @Override + public void setDozeScreenBrightnessFloat(float brightness) { + mDozeLog.traceDozeScreenBrightnessFloat(brightness); + mNotificationShadeWindowController.setDozeScreenBrightnessFloat(brightness); + } + @Override public void setAodDimmingScrim(float scrimOpacity) { mDozeLog.traceSetAodDimmingScrim(scrimOpacity); 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 41b69a733bd3..88a2b236d633 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -708,7 +708,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb * Shows the notification keyguard or the bouncer depending on * {@link #needsFullscreenBouncer()}. */ - protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing) { + protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing, boolean isFalsingReset) { boolean isDozing = mDozing; if (Flags.simPinRaceConditionOnRestart()) { KeyguardState toState = mKeyguardTransitionInteractor.getTransitionState().getValue() @@ -734,8 +734,12 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mPrimaryBouncerInteractor.show(/* isScrimmed= */ true); } } - } else { - Log.e(TAG, "Attempted to show the sim bouncer when it is already showing."); + } else if (!isFalsingReset) { + // Falsing resets can cause this to flicker, so don't reset in this case + Log.i(TAG, "Sim bouncer is already showing, issuing a refresh"); + mPrimaryBouncerInteractor.hide(); + mPrimaryBouncerInteractor.show(/* isScrimmed= */ true); + } } else { mCentralSurfaces.showKeyguard(); @@ -957,6 +961,10 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb @Override public void reset(boolean hideBouncerWhenShowing) { + reset(hideBouncerWhenShowing, /* isFalsingReset= */false); + } + + public void reset(boolean hideBouncerWhenShowing, boolean isFalsingReset) { if (mKeyguardStateController.isShowing() && !bouncerIsAnimatingAway()) { final boolean isOccluded = mKeyguardStateController.isOccluded(); // Hide quick settings. @@ -968,7 +976,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb hideBouncer(false /* destroyView */); } } else { - showBouncerOrKeyguard(hideBouncerWhenShowing); + showBouncerOrKeyguard(hideBouncerWhenShowing, isFalsingReset); } if (hideBouncerWhenShowing) { hideAlternateBouncer(true); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt index a7c5f78e5b69..03ec41d5af46 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt @@ -20,6 +20,7 @@ import android.os.OutcomeReceiver import android.telephony.TelephonyCallback import android.telephony.TelephonyManager import android.telephony.satellite.NtnSignalStrengthCallback +import android.telephony.satellite.SatelliteCommunicationAllowedStateCallback import android.telephony.satellite.SatelliteManager import android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_SUCCESS import android.telephony.satellite.SatelliteModemStateCallback @@ -37,7 +38,6 @@ import com.android.systemui.log.core.MessagePrinter import com.android.systemui.statusbar.pipeline.dagger.DeviceBasedSatelliteInputLog import com.android.systemui.statusbar.pipeline.dagger.VerboseDeviceBasedSatelliteInputLog import com.android.systemui.statusbar.pipeline.satellite.data.RealDeviceBasedSatelliteRepository -import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.POLLING_INTERVAL_MS import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Companion.whenSupported import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.NotSupported import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Supported @@ -60,11 +60,9 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn @@ -122,15 +120,9 @@ sealed interface SatelliteSupport { } /** - * Basically your everyday run-of-the-mill system service listener, with three notable exceptions. + * Basically your everyday run-of-the-mill system service listener, with two notable exceptions. * - * First, there is an availability bit that we are tracking via [SatelliteManager]. See - * [isSatelliteAllowedForCurrentLocation] for the implementation details. The thing to note about - * this bit is that there is no callback that exists. Therefore we implement a simple polling - * mechanism here. Since the underlying bit is location-dependent, we simply poll every hour (see - * [POLLING_INTERVAL_MS]) and see what the current state is. - * - * Secondly, there are cases when simply requesting information from SatelliteManager can fail. See + * First, there are cases when simply requesting information from SatelliteManager can fail. See * [SatelliteSupport] for details on how we track the state. What's worth noting here is that * SUPPORTED is a stronger guarantee than [satelliteManager] being null. Therefore, the fundamental * data flows here ([connectionState], [signalStrength],...) are wrapped in the convenience method @@ -138,7 +130,7 @@ sealed interface SatelliteSupport { * [SupportedSatelliteManager], we can guarantee that the manager is non-null AND that it has told * us that satellite is supported. Therefore, we don't expect exceptions to be thrown. * - * Lastly, this class is designed to wait a full minute of process uptime before making any requests + * Second, this class is designed to wait a full minute of process uptime before making any requests * to the satellite manager. The hope is that by waiting we don't have to retry due to a modem that * is still booting up or anything like that. We can tune or remove this behavior in the future if * necessary. @@ -158,8 +150,6 @@ constructor( private val satelliteManager: SatelliteManager? - override val isSatelliteAllowedForCurrentLocation: MutableStateFlow<Boolean> - // Some calls into satellite manager will throw exceptions if it is not supported. // This is never expected to change after boot, but may need to be retried in some cases @get:VisibleForTesting @@ -221,8 +211,6 @@ constructor( init { satelliteManager = satelliteManagerOpt.getOrNull() - isSatelliteAllowedForCurrentLocation = MutableStateFlow(false) - if (satelliteManager != null) { // Outer scope launch allows us to delay until MIN_UPTIME scope.launch { @@ -233,10 +221,7 @@ constructor( { "Checked for system support. support=$str1" }, ) - // Second, launch a job to poll for service availability based on location - scope.launch { pollForAvailabilityBasedOnLocation() } - - // Third, register a listener to let us know if there are changes to support + // Second, register a listener to let us know if there are changes to support scope.launch { listenForChangesToSatelliteSupport(satelliteManager) } } } else { @@ -259,28 +244,43 @@ constructor( return sm.checkSatelliteSupported() } - /* - * As there is no listener available for checking satellite allowed, we must poll the service. - * Defaulting to polling at most once every 20m while active. Subsequent OOS events will restart - * the job, so a flaky connection might cause more frequent checks. - */ - private suspend fun pollForAvailabilityBasedOnLocation() { + override val isSatelliteAllowedForCurrentLocation = satelliteSupport .whenSupported( - supported = ::isSatelliteAllowedHasListener, + supported = ::isSatelliteAvailableFlow, orElse = flowOf(false), retrySignal = telephonyProcessCrashedEvent, ) - .collectLatest { hasSubscribers -> - if (hasSubscribers) { - while (true) { - logBuffer.i { "requestIsCommunicationAllowedForCurrentLocation" } - checkIsSatelliteAllowed() - delay(POLLING_INTERVAL_MS) + .stateIn(scope, SharingStarted.Lazily, false) + + private fun isSatelliteAvailableFlow(sm: SupportedSatelliteManager): Flow<Boolean> = + conflatedCallbackFlow { + val callback = SatelliteCommunicationAllowedStateCallback { allowed -> + logBuffer.i({ bool1 = allowed }) { + "onSatelliteCommunicationAllowedStateChanged: $bool1" + } + + trySend(allowed) + } + + var registered = false + try { + sm.registerForCommunicationAllowedStateChanged( + bgDispatcher.asExecutor(), + callback + ) + registered = true + } catch (e: Exception) { + logBuffer.e("Error calling registerForCommunicationAllowedStateChanged", e) + } + + awaitClose { + if (registered) { + sm.unregisterForCommunicationAllowedStateChanged(callback) } } } - } + .flowOn(bgDispatcher) /** * Register a callback with [SatelliteManager] to let us know if there is a change in satellite @@ -410,14 +410,6 @@ constructor( } } - /** - * Signal that we should start polling [checkIsSatelliteAllowed]. We only need to poll if there - * are active listeners to [isSatelliteAllowedForCurrentLocation] - */ - @SuppressWarnings("unused") - private fun isSatelliteAllowedHasListener(sm: SupportedSatelliteManager): Flow<Boolean> = - isSatelliteAllowedForCurrentLocation.subscriptionCount.map { it > 0 }.distinctUntilChanged() - override val connectionState = satelliteSupport .whenSupported( @@ -485,28 +477,6 @@ constructor( } .flowOn(bgDispatcher) - /** Fire off a request to check for satellite availability. Always runs on the bg context */ - private suspend fun checkIsSatelliteAllowed() = - withContext(bgDispatcher) { - satelliteManager?.requestIsCommunicationAllowedForCurrentLocation( - bgDispatcher.asExecutor(), - object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> { - override fun onError(e: SatelliteManager.SatelliteException) { - logBuffer.e( - "Found exception when checking availability", - e, - ) - isSatelliteAllowedForCurrentLocation.value = false - } - - override fun onResult(allowed: Boolean) { - logBuffer.i { "isSatelliteAllowedForCurrentLocation: $allowed" } - isSatelliteAllowedForCurrentLocation.value = allowed - } - } - ) - } - private suspend fun SatelliteManager.checkSatelliteSupported(): SatelliteSupport = suspendCancellableCoroutine { continuation -> val cb = @@ -546,9 +516,6 @@ constructor( } companion object { - // TTL for satellite polling is twenty minutes - const val POLLING_INTERVAL_MS: Long = 1000 * 60 * 20 - // Let the system boot up and stabilize before we check for system support const val MIN_UPTIME: Long = 1000 * 60 diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt index f6934095b29d..21ec14fc7f03 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt @@ -289,6 +289,7 @@ interface PolicyModule { labelRes = R.string.quick_settings_work_mode_label, ), instanceId = uiEventLogger.getNewInstanceId(), + autoRemoveOnUnavailable = false, ) /** Inject work mode into tileViewModelMap in QSModule */ diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/TouchpadModule.kt b/packages/SystemUI/src/com/android/systemui/touchpad/TouchpadModule.kt new file mode 100644 index 000000000000..c86ac2f99a13 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/touchpad/TouchpadModule.kt @@ -0,0 +1,29 @@ +/* + * 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.touchpad + +import com.android.systemui.touchpad.data.repository.TouchpadRepository +import com.android.systemui.touchpad.data.repository.TouchpadRepositoryImpl +import dagger.Binds +import dagger.Module + +@Module +abstract class TouchpadModule { + + @Binds + abstract fun bindTouchpadRepository(repository: TouchpadRepositoryImpl): TouchpadRepository +} diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/data/repository/TouchpadRepository.kt b/packages/SystemUI/src/com/android/systemui/touchpad/data/repository/TouchpadRepository.kt new file mode 100644 index 000000000000..7131546ea816 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/touchpad/data/repository/TouchpadRepository.kt @@ -0,0 +1,59 @@ +/* + * 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.touchpad.data.repository + +import android.hardware.input.InputManager +import android.view.InputDevice.SOURCE_TOUCHPAD +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.inputdevice.data.repository.InputDeviceRepository +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +interface TouchpadRepository { + /** Emits true if any touchpad is connected to the device, false otherwise. */ + val isAnyTouchpadConnected: Flow<Boolean> +} + +@SysUISingleton +class TouchpadRepositoryImpl +@Inject +constructor( + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val inputManager: InputManager, + inputDeviceRepository: InputDeviceRepository +) : TouchpadRepository { + + override val isAnyTouchpadConnected: Flow<Boolean> = + inputDeviceRepository.deviceChange + .map { (ids, _) -> ids.any { id -> isTouchpad(id) } } + .distinctUntilChanged() + .flowOn(backgroundDispatcher) + + private fun isTouchpad(deviceId: Int): Boolean { + val device = inputManager.getInputDevice(deviceId) ?: return false + return device.supportsSource(SOURCE_TOUCHPAD) + } + + companion object { + const val TAG = "TouchpadRepositoryImpl" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt index 94ff65e3b32f..51dfef0b6b6a 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -49,6 +50,7 @@ import androidx.compose.ui.input.pointer.pointerInteropFilter import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.airbnb.lottie.LottieComposition import com.airbnb.lottie.LottieProperty import com.airbnb.lottie.compose.LottieAnimation import com.airbnb.lottie.compose.LottieCompositionSpec @@ -61,6 +63,9 @@ import com.airbnb.lottie.compose.rememberLottieDynamicProperties import com.airbnb.lottie.compose.rememberLottieDynamicProperty import com.android.compose.theme.LocalAndroidColorScheme import com.android.systemui.res.R +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.IN_PROGRESS import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGesture.BACK import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureHandler @@ -78,23 +83,49 @@ fun BackGestureTutorialScreen( ) { val screenColors = rememberScreenColors() BackHandler(onBack = onBack) - var gestureDone by remember { mutableStateOf(false) } + var gestureState by remember { mutableStateOf(GestureState.NOT_STARTED) } val swipeDistanceThresholdPx = LocalContext.current.resources.getDimensionPixelSize( com.android.internal.R.dimen.system_gestures_distance_threshold ) val gestureHandler = remember(swipeDistanceThresholdPx) { - TouchpadGestureHandler(BACK, swipeDistanceThresholdPx, onDone = { gestureDone = true }) + TouchpadGestureHandler( + BACK, + swipeDistanceThresholdPx, + onGestureStateChanged = { gestureState = it } + ) } + TouchpadGesturesHandlingBox(gestureHandler, gestureState) { + GestureTutorialContent(gestureState, onDoneButtonClicked, screenColors) + } +} + +@Composable +private fun TouchpadGesturesHandlingBox( + gestureHandler: TouchpadGestureHandler, + gestureState: GestureState, + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit +) { Box( modifier = - Modifier.fillMaxSize() + modifier + .fillMaxSize() // we need to use pointerInteropFilter because some info about touchpad gestures is // only available in MotionEvent - .pointerInteropFilter(onTouchEvent = gestureHandler::onMotionEvent) + .pointerInteropFilter( + onTouchEvent = { event -> + // FINISHED is the final state so we don't need to process touches anymore + if (gestureState != FINISHED) { + gestureHandler.onMotionEvent(event) + } else { + false + } + } + ) ) { - GestureTutorialContent(gestureDone, onDoneButtonClicked, screenColors) + content() } } @@ -126,14 +157,14 @@ private fun rememberScreenColors(): TutorialScreenColors { @Composable private fun GestureTutorialContent( - gestureDone: Boolean, + gestureState: GestureState, onDoneButtonClicked: () -> Unit, screenColors: TutorialScreenColors ) { val animatedColor by animateColorAsState( targetValue = - if (gestureDone) screenColors.successBackgroundColor + if (gestureState == FINISHED) screenColors.successBackgroundColor else screenColors.backgroundColor, animationSpec = tween(durationMillis = 150, easing = LinearEasing), label = "backgroundColor" @@ -148,15 +179,17 @@ private fun GestureTutorialContent( Row(modifier = Modifier.fillMaxWidth().weight(1f)) { TutorialDescription( titleTextId = - if (gestureDone) R.string.touchpad_tutorial_gesture_done + if (gestureState == FINISHED) R.string.touchpad_tutorial_gesture_done else R.string.touchpad_back_gesture_action_title, titleColor = screenColors.titleColor, - bodyTextId = R.string.touchpad_back_gesture_guidance, + bodyTextId = + if (gestureState == FINISHED) R.string.touchpad_back_gesture_finished + else R.string.touchpad_back_gesture_guidance, modifier = Modifier.weight(1f) ) Spacer(modifier = Modifier.width(76.dp)) TutorialAnimation( - gestureDone, + gestureState, screenColors.animationProperties, modifier = Modifier.weight(1f).padding(top = 8.dp) ) @@ -189,27 +222,38 @@ fun TutorialDescription( @Composable fun TutorialAnimation( - gestureDone: Boolean, + gestureState: GestureState, animationProperties: LottieDynamicProperties, modifier: Modifier = Modifier ) { Column(modifier = modifier.fillMaxWidth()) { - val resId = if (gestureDone) R.raw.trackpad_back_success else R.raw.trackpad_back_edu + val resId = + if (gestureState == FINISHED) R.raw.trackpad_back_success else R.raw.trackpad_back_edu val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(resId)) - val progress by - animateLottieCompositionAsState( - composition, - iterations = if (gestureDone) 1 else LottieConstants.IterateForever - ) + val progress = progressForGestureState(composition, gestureState) LottieAnimation( composition = composition, - progress = { progress }, + progress = progress, dynamicProperties = animationProperties ) } } @Composable +private fun progressForGestureState( + composition: LottieComposition?, + gestureState: GestureState +): () -> Float { + if (gestureState == IN_PROGRESS) { + return { 0f } // when gesture is in progress, animation should freeze on 1st frame + } else { + val iterations = if (gestureState == FINISHED) 1 else LottieConstants.IterateForever + val animationState by animateLottieCompositionAsState(composition, iterations = iterations) + return { animationState } + } +} + +@Composable fun rememberColorFilterProperty( layerName: String, color: Color diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt index 1fa7a0c44171..6fa9bcd23045 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt @@ -17,23 +17,26 @@ package com.android.systemui.touchpad.tutorial.ui.gesture import android.view.MotionEvent +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.IN_PROGRESS +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NOT_STARTED import kotlin.math.abs /** - * Monitor for touchpad gestures that calls [gestureDoneCallback] when gesture was successfully - * done. All tracked motion events should be passed to [processTouchpadEvent] + * Monitor for touchpad gestures that calls [gestureStateChangedCallback] when [GestureState] + * changes. All tracked motion events should be passed to [processTouchpadEvent] */ interface TouchpadGestureMonitor { val gestureDistanceThresholdPx: Int - val gestureDoneCallback: () -> Unit + val gestureStateChangedCallback: (GestureState) -> Unit fun processTouchpadEvent(event: MotionEvent) } class BackGestureMonitor( override val gestureDistanceThresholdPx: Int, - override val gestureDoneCallback: () -> Unit + override val gestureStateChangedCallback: (GestureState) -> Unit ) : TouchpadGestureMonitor { private var xStart = 0f @@ -44,13 +47,16 @@ class BackGestureMonitor( MotionEvent.ACTION_DOWN -> { if (isThreeFingerTouchpadSwipe(event)) { xStart = event.x + gestureStateChangedCallback(IN_PROGRESS) } } MotionEvent.ACTION_UP -> { if (isThreeFingerTouchpadSwipe(event)) { val distance = abs(event.x - xStart) if (distance >= gestureDistanceThresholdPx) { - gestureDoneCallback() + gestureStateChangedCallback(FINISHED) + } else { + gestureStateChangedCallback(NOT_STARTED) } } } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt new file mode 100644 index 000000000000..446875af66e7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt @@ -0,0 +1,23 @@ +/* + * 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.touchpad.tutorial.ui.gesture + +enum class GestureState { + NOT_STARTED, + IN_PROGRESS, + FINISHED +} diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt index 4ae9c7b2426c..190da62aca92 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt @@ -22,10 +22,10 @@ enum class TouchpadGesture { fun toMonitor( swipeDistanceThresholdPx: Int, - gestureDoneCallback: () -> Unit + onStateChanged: (GestureState) -> Unit ): TouchpadGestureMonitor { return when (this) { - BACK -> BackGestureMonitor(swipeDistanceThresholdPx, gestureDoneCallback) + BACK -> BackGestureMonitor(swipeDistanceThresholdPx, onStateChanged) else -> throw IllegalArgumentException("Not implemented yet") } } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt index dc8471c3248a..cac2a99bc02c 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt @@ -26,11 +26,11 @@ import android.view.MotionEvent class TouchpadGestureHandler( touchpadGesture: TouchpadGesture, swipeDistanceThresholdPx: Int, - onDone: () -> Unit + onGestureStateChanged: (GestureState) -> Unit ) { private val gestureRecognition = - touchpadGesture.toMonitor(swipeDistanceThresholdPx, gestureDoneCallback = onDone) + touchpadGesture.toMonitor(swipeDistanceThresholdPx, onStateChanged = onGestureStateChanged) fun onMotionEvent(event: MotionEvent): Boolean { // events from touchpad have SOURCE_MOUSE and not SOURCE_TOUCHPAD because of legacy reasons diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt index efaca7a8239f..5d8b6f144d97 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt @@ -19,8 +19,8 @@ package com.android.systemui.volume.dagger import android.content.ContentResolver import android.content.Context import android.media.AudioManager -import com.android.settingslib.bluetooth.BluetoothUtils import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.settingslib.flags.Flags import com.android.settingslib.notification.domain.interactor.NotificationsSoundPolicyInteractor import com.android.settingslib.volume.data.repository.AudioRepository import com.android.settingslib.volume.data.repository.AudioRepositoryImpl @@ -80,7 +80,7 @@ interface AudioModule { @Application coroutineScope: CoroutineScope, @Background coroutineContext: CoroutineContext, ): AudioSharingRepository = - if (BluetoothUtils.isAudioSharingEnabled() && localBluetoothManager != null) { + if (Flags.enableLeAudioSharing() && localBluetoothManager != null) { AudioSharingRepositoryImpl( contentResolver, localBluetoothManager, diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java index 5ea5c2189560..d3b7d2207854 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java @@ -52,6 +52,7 @@ import android.widget.Spinner; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.internal.logging.UiEventLogger; import com.android.settingslib.bluetooth.BluetoothEventManager; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; @@ -123,6 +124,8 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { @Mock private AudioManager mAudioManager; @Mock + private UiEventLogger mUiEventLogger; + @Mock private CachedBluetoothDevice mCachedDevice; @Mock private BluetoothDevice mDevice; @@ -179,6 +182,7 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { anyInt(), any()); assertThat(intentCaptor.getValue().getAction()).isEqualTo( Settings.ACTION_HEARING_DEVICE_PAIRING_SETTINGS); + verify(mUiEventLogger).log(HearingDevicesUiEvent.HEARING_DEVICES_PAIR); } @Test @@ -192,7 +196,7 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { anyInt(), any()); assertThat(intentCaptor.getValue().getAction()).isEqualTo( HearingDevicesDialogDelegate.ACTION_BLUETOOTH_DEVICE_DETAILS); - + verify(mUiEventLogger).log(HearingDevicesUiEvent.HEARING_DEVICES_GEAR_CLICK); } @Test @@ -200,9 +204,10 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { setUpDeviceListDialog(); when(mHearingDeviceItem.getType()).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE); - mDialogDelegate.onDeviceItemOnClicked(mHearingDeviceItem, new View(mContext)); + mDialogDelegate.onDeviceItemClicked(mHearingDeviceItem, new View(mContext)); verify(mCachedDevice).disconnect(); + verify(mUiEventLogger).log(HearingDevicesUiEvent.HEARING_DEVICES_DISCONNECT); } @Test @@ -304,7 +309,8 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { mDialogTransitionAnimator, mLocalBluetoothManager, new Handler(mTestableLooper.getLooper()), - mAudioManager + mAudioManager, + mUiEventLogger ); mDialog = mDialogDelegate.createDialog(); @@ -326,7 +332,8 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { mDialogTransitionAnimator, mLocalBluetoothManager, new Handler(mTestableLooper.getLooper()), - mAudioManager + mAudioManager, + mUiEventLogger ); mDialog = mDialogDelegate.createDialog(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java index aa5edae72684..4818119045a5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java @@ -42,13 +42,21 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Intent; +import android.hardware.display.DisplayManager; import android.os.PowerManager; import android.os.UserHandle; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.Settings; +import android.view.Display; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.internal.display.BrightnessSynchronizer; +import com.android.server.display.feature.flags.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.dock.DockManager; import com.android.systemui.keyguard.WakefulnessLifecycle; @@ -62,6 +70,7 @@ import com.android.systemui.util.settings.SystemSettings; import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -74,10 +83,15 @@ import java.util.Optional; @RunWith(AndroidJUnit4.class) public class DozeScreenBrightnessTest extends SysuiTestCase { - private static final int DEFAULT_BRIGHTNESS = 10; - private static final int DIM_BRIGHTNESS = 1; - private static final int[] SENSOR_TO_BRIGHTNESS = new int[]{-1, 1, 2, 3, 4}; + private static final int DEFAULT_BRIGHTNESS_INT = 10; + private static final float DEFAULT_BRIGHTNESS_FLOAT = 0.1f; + private static final int DIM_BRIGHTNESS_INT = 1; + private static final float DIM_BRIGHTNESS_FLOAT = 0.05f; + private static final int[] SENSOR_TO_BRIGHTNESS_INT = new int[]{-1, 1, 2, 3, 4}; + private static final float[] SENSOR_TO_BRIGHTNESS_FLOAT = + new float[]{-1, 0.01f, 0.05f, 0.7f, 0.1f}; private static final int[] SENSOR_TO_OPACITY = new int[]{-1, 10, 0, 0, 0}; + private static final float DELTA = BrightnessSynchronizer.EPSILON; private DozeServiceFake mServiceFake; private FakeSensorManager.FakeGenericSensor mSensor; @@ -98,16 +112,23 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { DozeLog mDozeLog; @Mock SystemSettings mSystemSettings; + @Mock + DisplayManager mDisplayManager; private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock()); private FakeThreadFactory mFakeThreadFactory = new FakeThreadFactory(mFakeExecutor); private DozeScreenBrightness mScreen; + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); when(mSystemSettings.getIntForUser(eq(Settings.System.SCREEN_BRIGHTNESS), anyInt(), - eq(UserHandle.USER_CURRENT))).thenReturn(DEFAULT_BRIGHTNESS); + eq(UserHandle.USER_CURRENT))).thenReturn(PowerManager.BRIGHTNESS_ON); + when(mDisplayManager.getBrightness(Display.DEFAULT_DISPLAY)) + .thenReturn(PowerManager.BRIGHTNESS_MAX); doAnswer(invocation -> { ((Runnable) invocation.getArgument(0)).run(); return null; @@ -117,9 +138,14 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { mSensorManager = new AsyncSensorManager(fakeSensorManager, mFakeThreadFactory, null); mAlwaysOnDisplayPolicy = new AlwaysOnDisplayPolicy(mContext); - mAlwaysOnDisplayPolicy.defaultDozeBrightness = DEFAULT_BRIGHTNESS; - mAlwaysOnDisplayPolicy.screenBrightnessArray = SENSOR_TO_BRIGHTNESS; - mAlwaysOnDisplayPolicy.dimBrightness = DIM_BRIGHTNESS; + mAlwaysOnDisplayPolicy.defaultDozeBrightness = DEFAULT_BRIGHTNESS_INT; + when(mDisplayManager.getDefaultDozeBrightness(Display.DEFAULT_DISPLAY)) + .thenReturn(DEFAULT_BRIGHTNESS_FLOAT); + mAlwaysOnDisplayPolicy.screenBrightnessArray = SENSOR_TO_BRIGHTNESS_INT; + when(mDisplayManager.getDozeBrightnessSensorValueToBrightness(Display.DEFAULT_DISPLAY)) + .thenReturn(SENSOR_TO_BRIGHTNESS_FLOAT); + mAlwaysOnDisplayPolicy.dimBrightness = DIM_BRIGHTNESS_INT; + mAlwaysOnDisplayPolicy.dimBrightnessFloat = DIM_BRIGHTNESS_FLOAT; mAlwaysOnDisplayPolicy.dimmingScrimArray = SENSOR_TO_OPACITY; mSensor = fakeSensorManager.getFakeLightSensor(); mSensorInner = fakeSensorManager.getFakeLightSensor2(); @@ -135,19 +161,51 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { mDozeParameters, mDevicePostureController, mDozeLog, - mSystemSettings); + mSystemSettings, + mDisplayManager); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void testInitialize_setsScreenBrightnessToValidValue_Int() { + mScreen.transitionTo(UNINITIALIZED, INITIALIZED); + + assertEquals(DEFAULT_BRIGHTNESS_INT, mServiceFake.screenBrightnessInt); + assertTrue(mServiceFake.screenBrightnessInt >= PowerManager.BRIGHTNESS_OFF + 1); + assertTrue(mServiceFake.screenBrightnessInt <= PowerManager.BRIGHTNESS_ON); + assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat)); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void testInitialize_setsScreenBrightnessToValidValue_Float() { + mScreen.transitionTo(UNINITIALIZED, INITIALIZED); + + assertEquals(DEFAULT_BRIGHTNESS_FLOAT, mServiceFake.screenBrightnessFloat, DELTA); + assertTrue(mServiceFake.screenBrightnessFloat >= PowerManager.BRIGHTNESS_MIN); + assertTrue(mServiceFake.screenBrightnessFloat <= PowerManager.BRIGHTNESS_MAX); + assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt); } @Test - public void testInitialize_setsScreenBrightnessToValidValue() throws Exception { + @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void testAod_usesDebugValue_Int() { mScreen.transitionTo(UNINITIALIZED, INITIALIZED); + mScreen.transitionTo(INITIALIZED, DOZE_AOD); + waitForSensorManager(); - assertEquals(DEFAULT_BRIGHTNESS, mServiceFake.screenBrightness); - assertTrue(mServiceFake.screenBrightness <= PowerManager.BRIGHTNESS_ON); + Intent intent = new Intent(DozeScreenBrightness.ACTION_AOD_BRIGHTNESS); + intent.putExtra(DozeScreenBrightness.BRIGHTNESS_BUCKET, 1); + mScreen.onReceive(mContext, intent); + mSensor.sendSensorEvent(3); + + assertEquals(SENSOR_TO_BRIGHTNESS_INT[1], mServiceFake.screenBrightnessInt); + assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat)); } @Test - public void testAod_usesDebugValue() throws Exception { + @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void testAod_usesDebugValue_Float() { mScreen.transitionTo(UNINITIALIZED, INITIALIZED); mScreen.transitionTo(INITIALIZED, DOZE_AOD); waitForSensorManager(); @@ -157,11 +215,13 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { mScreen.onReceive(mContext, intent); mSensor.sendSensorEvent(3); - assertEquals(1, mServiceFake.screenBrightness); + assertEquals(SENSOR_TO_BRIGHTNESS_FLOAT[1], mServiceFake.screenBrightnessFloat, DELTA); + assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt); } @Test - public void testAod_usesLightSensorRespectingUserSetting() { + @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void testAod_usesLightSensorRespectingUserSetting_Int() { int maxBrightness = 3; when(mSystemSettings.getIntForUser(eq(Settings.System.SCREEN_BRIGHTNESS), anyInt(), eq(UserHandle.USER_CURRENT))).thenReturn(maxBrightness); @@ -170,11 +230,27 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { .thenReturn(Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL); mScreen.transitionTo(UNINITIALIZED, INITIALIZED); - assertEquals(maxBrightness, mServiceFake.screenBrightness); + assertEquals(maxBrightness, mServiceFake.screenBrightnessInt); + assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat)); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void testAod_usesLightSensorRespectingUserSetting_Float() { + float maxBrightness = DEFAULT_BRIGHTNESS_FLOAT / 2; + when(mDisplayManager.getBrightness(Display.DEFAULT_DISPLAY)).thenReturn(maxBrightness); + when(mSystemSettings.getIntForUser(eq(Settings.System.SCREEN_BRIGHTNESS_MODE), anyInt(), + eq(UserHandle.USER_CURRENT))) + .thenReturn(Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL); + + mScreen.transitionTo(UNINITIALIZED, INITIALIZED); + assertEquals(maxBrightness, mServiceFake.screenBrightnessFloat, DELTA); + assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt); } @Test - public void testAod_usesLightSensorNotClampingToAutoBrightnessValue() { + @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void testAod_usesLightSensorNotClampingToAutoBrightnessValue_Int() { int maxBrightness = 3; when(mSystemSettings.getIntForUser(eq(Settings.System.SCREEN_BRIGHTNESS), anyInt(), eq(UserHandle.USER_CURRENT))).thenReturn(maxBrightness); @@ -183,11 +259,27 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { .thenReturn(Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC); mScreen.transitionTo(UNINITIALIZED, INITIALIZED); - assertEquals(DEFAULT_BRIGHTNESS, mServiceFake.screenBrightness); + assertEquals(DEFAULT_BRIGHTNESS_INT, mServiceFake.screenBrightnessInt); + assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat)); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void testAod_usesLightSensorNotClampingToAutoBrightnessValue_Float() { + float maxBrightness = DEFAULT_BRIGHTNESS_FLOAT / 2; + when(mDisplayManager.getBrightness(Display.DEFAULT_DISPLAY)).thenReturn(maxBrightness); + when(mSystemSettings.getIntForUser(eq(Settings.System.SCREEN_BRIGHTNESS_MODE), anyInt(), + eq(UserHandle.USER_CURRENT))) + .thenReturn(Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC); + + mScreen.transitionTo(UNINITIALIZED, INITIALIZED); + assertEquals(DEFAULT_BRIGHTNESS_FLOAT, mServiceFake.screenBrightnessFloat, DELTA); + assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt); } @Test - public void doze_doesNotUseLightSensor() { + @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void doze_doesNotUseLightSensor_Int() { // GIVEN the device is DOZE and the display state changes to ON mScreen.transitionTo(UNINITIALIZED, INITIALIZED); mScreen.transitionTo(INITIALIZED, DOZE); @@ -197,12 +289,48 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { mSensor.sendSensorEvent(3); // THEN brightness is NOT changed, it's set to the default brightness - assertNotSame(3, mServiceFake.screenBrightness); - assertEquals(DEFAULT_BRIGHTNESS, mServiceFake.screenBrightness); + assertNotSame(SENSOR_TO_BRIGHTNESS_INT[3], mServiceFake.screenBrightnessInt); + assertEquals(DEFAULT_BRIGHTNESS_INT, mServiceFake.screenBrightnessInt); + assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat)); } @Test - public void dozeSuspendTriggers_doesNotUseLightSensor() { + @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void doze_doesNotUseLightSensor_Float() { + // GIVEN the device is DOZE and the display state changes to ON + mScreen.transitionTo(UNINITIALIZED, INITIALIZED); + mScreen.transitionTo(INITIALIZED, DOZE); + waitForSensorManager(); + + // WHEN new sensor event sent + mSensor.sendSensorEvent(3); + + // THEN brightness is NOT changed, it's set to the default brightness + assertNotSame(SENSOR_TO_BRIGHTNESS_FLOAT[3], mServiceFake.screenBrightnessInt); + assertEquals(DEFAULT_BRIGHTNESS_FLOAT, mServiceFake.screenBrightnessFloat, DELTA); + assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void dozeSuspendTriggers_doesNotUseLightSensor_Int() { + // GIVEN the device is DOZE and the display state changes to ON + mScreen.transitionTo(UNINITIALIZED, INITIALIZED); + mScreen.transitionTo(INITIALIZED, DOZE_SUSPEND_TRIGGERS); + waitForSensorManager(); + + // WHEN new sensor event sent + mSensor.sendSensorEvent(3); + + // THEN brightness is NOT changed, it's set to the default brightness + assertNotSame(SENSOR_TO_BRIGHTNESS_INT[3], mServiceFake.screenBrightnessInt); + assertEquals(DEFAULT_BRIGHTNESS_INT, mServiceFake.screenBrightnessInt); + assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat)); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void dozeSuspendTriggers_doesNotUseLightSensor_Float() { // GIVEN the device is DOZE and the display state changes to ON mScreen.transitionTo(UNINITIALIZED, INITIALIZED); mScreen.transitionTo(INITIALIZED, DOZE_SUSPEND_TRIGGERS); @@ -212,12 +340,14 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { mSensor.sendSensorEvent(3); // THEN brightness is NOT changed, it's set to the default brightness - assertNotSame(3, mServiceFake.screenBrightness); - assertEquals(DEFAULT_BRIGHTNESS, mServiceFake.screenBrightness); + assertNotSame(SENSOR_TO_BRIGHTNESS_FLOAT[3], mServiceFake.screenBrightnessFloat); + assertEquals(DEFAULT_BRIGHTNESS_FLOAT, mServiceFake.screenBrightnessFloat, DELTA); + assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt); } @Test - public void aod_usesLightSensor() { + @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void aod_usesLightSensor_Int() { // GIVEN the device is DOZE_AOD and the display state changes to ON mScreen.transitionTo(UNINITIALIZED, INITIALIZED); mScreen.transitionTo(INITIALIZED, DOZE_AOD); @@ -227,11 +357,29 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { mSensor.sendSensorEvent(3); // THEN brightness is updated - assertEquals(3, mServiceFake.screenBrightness); + assertEquals(SENSOR_TO_BRIGHTNESS_INT[3], mServiceFake.screenBrightnessInt); + assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat)); } @Test - public void docked_usesLightSensor() { + @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void aod_usesLightSensor_Float() { + // GIVEN the device is DOZE_AOD and the display state changes to ON + mScreen.transitionTo(UNINITIALIZED, INITIALIZED); + mScreen.transitionTo(INITIALIZED, DOZE_AOD); + waitForSensorManager(); + + // WHEN new sensor event sent + mSensor.sendSensorEvent(3); + + // THEN brightness is updated + assertEquals(SENSOR_TO_BRIGHTNESS_FLOAT[3], mServiceFake.screenBrightnessFloat, DELTA); + assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void docked_usesLightSensor_Int() { // GIVEN the device is docked and the display state changes to ON mScreen.transitionTo(UNINITIALIZED, INITIALIZED); mScreen.transitionTo(INITIALIZED, DOZE_AOD); @@ -242,11 +390,29 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { mSensor.sendSensorEvent(3); // THEN brightness is updated - assertEquals(3, mServiceFake.screenBrightness); + assertEquals(SENSOR_TO_BRIGHTNESS_INT[3], mServiceFake.screenBrightnessInt); + assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat)); } @Test - public void testPulsing_withoutLightSensor_setsAoDDimmingScrimTransparent() throws Exception { + @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void docked_usesLightSensor_Float() { + // GIVEN the device is docked and the display state changes to ON + mScreen.transitionTo(UNINITIALIZED, INITIALIZED); + mScreen.transitionTo(INITIALIZED, DOZE_AOD); + mScreen.transitionTo(DOZE_AOD, DOZE_AOD_DOCKED); + waitForSensorManager(); + + // WHEN new sensor event sent + mSensor.sendSensorEvent(3); + + // THEN brightness is updated + assertEquals(SENSOR_TO_BRIGHTNESS_FLOAT[3], mServiceFake.screenBrightnessFloat, DELTA); + assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt); + } + + @Test + public void testPulsing_withoutLightSensor_setsAoDDimmingScrimTransparent() { mScreen = new DozeScreenBrightness( mContext, mServiceFake, @@ -258,7 +424,8 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { mDozeParameters, mDevicePostureController, mDozeLog, - mSystemSettings); + mSystemSettings, + mDisplayManager); mScreen.transitionTo(UNINITIALIZED, INITIALIZED); mScreen.transitionTo(INITIALIZED, DOZE); reset(mDozeHost); @@ -269,7 +436,8 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { } @Test - public void testScreenOffAfterPulsing_pausesLightSensor() throws Exception { + @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void testScreenOffAfterPulsing_pausesLightSensor_Int() { mScreen.transitionTo(UNINITIALIZED, INITIALIZED); mScreen.transitionTo(INITIALIZED, DOZE); mScreen.transitionTo(DOZE, DOZE_REQUEST_PULSE); @@ -280,11 +448,29 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { mSensor.sendSensorEvent(1); - assertEquals(DEFAULT_BRIGHTNESS, mServiceFake.screenBrightness); + assertEquals(DEFAULT_BRIGHTNESS_INT, mServiceFake.screenBrightnessInt); + assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat)); } @Test - public void testNullSensor() throws Exception { + @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void testScreenOffAfterPulsing_pausesLightSensor_Float() { + mScreen.transitionTo(UNINITIALIZED, INITIALIZED); + mScreen.transitionTo(INITIALIZED, DOZE); + mScreen.transitionTo(DOZE, DOZE_REQUEST_PULSE); + mScreen.transitionTo(DOZE_REQUEST_PULSE, DOZE_PULSING); + mScreen.transitionTo(DOZE_PULSING, DOZE_PULSE_DONE); + mScreen.transitionTo(DOZE_PULSE_DONE, DOZE); + waitForSensorManager(); + + mSensor.sendSensorEvent(1); + + assertEquals(DEFAULT_BRIGHTNESS_FLOAT, mServiceFake.screenBrightnessFloat, DELTA); + assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt); + } + + @Test + public void testNullSensor() { mScreen = new DozeScreenBrightness( mContext, mServiceFake, @@ -296,7 +482,8 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { mDozeParameters, mDevicePostureController, mDozeLog, - mSystemSettings); + mSystemSettings, + mDisplayManager); mScreen.transitionTo(UNINITIALIZED, INITIALIZED); mScreen.transitionTo(INITIALIZED, DOZE_AOD); @@ -305,7 +492,50 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { } @Test - public void testSensorsSupportPostures_closed() throws Exception { + @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void testSensorsSupportPostures_closed_Int() { + // GIVEN the device is CLOSED + when(mDevicePostureController.getDevicePosture()).thenReturn( + DevicePostureController.DEVICE_POSTURE_CLOSED); + + // GIVEN closed and opened postures use different light sensors + mScreen = new DozeScreenBrightness( + mContext, + mServiceFake, + mSensorManager, + new Optional[]{ + Optional.empty() /* unknown */, + Optional.of(mSensor.getSensor()) /* closed */, + Optional.of(mSensorInner.getSensor()) /* half-opened */, + Optional.of(mSensorInner.getSensor()) /* opened */, + Optional.empty() /* flipped */ + }, + mDozeHost, null /* handler */, + mAlwaysOnDisplayPolicy, + mWakefulnessLifecycle, + mDozeParameters, + mDevicePostureController, + mDozeLog, + mSystemSettings, + mDisplayManager); + + // GIVEN the device is in AOD + mScreen.transitionTo(UNINITIALIZED, INITIALIZED); + mScreen.transitionTo(INITIALIZED, DOZE_AOD); + waitForSensorManager(); + + // WHEN new different events are sent from the inner and outer sensors + mSensor.sendSensorEvent(3); // CLOSED sensor + mSensorInner.sendSensorEvent(4); // OPENED sensor + + // THEN brightness is updated according to the sensor for CLOSED + assertEquals(SENSOR_TO_BRIGHTNESS_INT[3], mServiceFake.screenBrightnessInt); + assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat)); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void testSensorsSupportPostures_closed_Float() { // GIVEN the device is CLOSED when(mDevicePostureController.getDevicePosture()).thenReturn( DevicePostureController.DEVICE_POSTURE_CLOSED); @@ -328,7 +558,8 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { mDozeParameters, mDevicePostureController, mDozeLog, - mSystemSettings); + mSystemSettings, + mDisplayManager); // GIVEN the device is in AOD mScreen.transitionTo(UNINITIALIZED, INITIALIZED); @@ -340,11 +571,14 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { mSensorInner.sendSensorEvent(4); // OPENED sensor // THEN brightness is updated according to the sensor for CLOSED - assertEquals(3, mServiceFake.screenBrightness); + assertEquals(SENSOR_TO_BRIGHTNESS_FLOAT[3], mServiceFake.screenBrightnessFloat, + DELTA); + assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt); } @Test - public void testSensorsSupportPostures_open() throws Exception { + @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void testSensorsSupportPostures_open_Int() { // GIVEN the device is OPENED when(mDevicePostureController.getDevicePosture()).thenReturn( DevicePostureController.DEVICE_POSTURE_OPENED); @@ -367,7 +601,8 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { mDozeParameters, mDevicePostureController, mDozeLog, - mSystemSettings); + mSystemSettings, + mDisplayManager); // GIVEN device is in AOD mScreen.transitionTo(UNINITIALIZED, INITIALIZED); @@ -379,11 +614,55 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { mSensor.sendSensorEvent(3); // CLOSED sensor // THEN brightness is updated according to the sensor for OPENED - assertEquals(4, mServiceFake.screenBrightness); + assertEquals(SENSOR_TO_BRIGHTNESS_INT[4], mServiceFake.screenBrightnessInt); + assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat)); } @Test - public void testSensorsSupportPostures_swapPostures() throws Exception { + @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void testSensorsSupportPostures_open_Float() { + // GIVEN the device is OPENED + when(mDevicePostureController.getDevicePosture()).thenReturn( + DevicePostureController.DEVICE_POSTURE_OPENED); + + // GIVEN closed and opened postures use different light sensors + mScreen = new DozeScreenBrightness( + mContext, + mServiceFake, + mSensorManager, + new Optional[]{ + Optional.empty() /* unknown */, + Optional.of(mSensor.getSensor()) /* closed */, + Optional.of(mSensorInner.getSensor()) /* half-opened */, + Optional.of(mSensorInner.getSensor()) /* opened */, + Optional.empty() /* flipped */ + }, + mDozeHost, null /* handler */, + mAlwaysOnDisplayPolicy, + mWakefulnessLifecycle, + mDozeParameters, + mDevicePostureController, + mDozeLog, + mSystemSettings, + mDisplayManager); + + // GIVEN device is in AOD + mScreen.transitionTo(UNINITIALIZED, INITIALIZED); + mScreen.transitionTo(INITIALIZED, DOZE_AOD); + waitForSensorManager(); + + // WHEN new different events are sent from the inner and outer sensors + mSensorInner.sendSensorEvent(4); // OPENED sensor + mSensor.sendSensorEvent(3); // CLOSED sensor + + // THEN brightness is updated according to the sensor for OPENED + assertEquals(SENSOR_TO_BRIGHTNESS_FLOAT[4], mServiceFake.screenBrightnessFloat, DELTA); + assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void testSensorsSupportPostures_swapPostures_Int() { ArgumentCaptor<DevicePostureController.Callback> postureCallbackCaptor = ArgumentCaptor.forClass(DevicePostureController.Callback.class); reset(mDevicePostureController); @@ -410,7 +689,8 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { mDozeParameters, mDevicePostureController, mDozeLog, - mSystemSettings); + mSystemSettings, + mDisplayManager); verify(mDevicePostureController).addCallback(postureCallbackCaptor.capture()); // GIVEN device is in AOD @@ -428,11 +708,79 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { mSensorInner.sendSensorEvent(4); // OPENED sensor // THEN brightness is updated according to the sensor for CLOSED - assertEquals(3, mServiceFake.screenBrightness); + assertEquals(SENSOR_TO_BRIGHTNESS_INT[3], mServiceFake.screenBrightnessInt); + assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat)); } @Test - public void testNoBrightnessDeliveredAfterFinish() throws Exception { + @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void testSensorsSupportPostures_swapPostures_Float() { + ArgumentCaptor<DevicePostureController.Callback> postureCallbackCaptor = + ArgumentCaptor.forClass(DevicePostureController.Callback.class); + reset(mDevicePostureController); + + // GIVEN the device starts up AOD OPENED + when(mDevicePostureController.getDevicePosture()).thenReturn( + DevicePostureController.DEVICE_POSTURE_OPENED); + + // GIVEN closed and opened postures use different light sensors + mScreen = new DozeScreenBrightness( + mContext, + mServiceFake, + mSensorManager, + new Optional[]{ + Optional.empty() /* unknown */, + Optional.of(mSensor.getSensor()) /* closed */, + Optional.of(mSensorInner.getSensor()) /* half-opened */, + Optional.of(mSensorInner.getSensor()) /* opened */, + Optional.empty() /* flipped */ + }, + mDozeHost, null /* handler */, + mAlwaysOnDisplayPolicy, + mWakefulnessLifecycle, + mDozeParameters, + mDevicePostureController, + mDozeLog, + mSystemSettings, + mDisplayManager); + verify(mDevicePostureController).addCallback(postureCallbackCaptor.capture()); + + // GIVEN device is in AOD + mScreen.transitionTo(UNINITIALIZED, INITIALIZED); + mScreen.transitionTo(INITIALIZED, DOZE_AOD); + waitForSensorManager(); + + // WHEN the posture changes to CLOSED + postureCallbackCaptor.getValue().onPostureChanged( + DevicePostureController.DEVICE_POSTURE_CLOSED); + waitForSensorManager(); + + // WHEN new different events are sent from the inner and outer sensors + mSensor.sendSensorEvent(3); // CLOSED sensor + mSensorInner.sendSensorEvent(4); // OPENED sensor + + // THEN brightness is updated according to the sensor for CLOSED + assertEquals(SENSOR_TO_BRIGHTNESS_FLOAT[3], mServiceFake.screenBrightnessFloat, DELTA); + assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void testNoBrightnessDeliveredAfterFinish_Int() { + mScreen.transitionTo(UNINITIALIZED, INITIALIZED); + mScreen.transitionTo(INITIALIZED, DOZE_AOD); + mScreen.transitionTo(DOZE_AOD, FINISH); + waitForSensorManager(); + + mSensor.sendSensorEvent(1); + + assertNotEquals(SENSOR_TO_BRIGHTNESS_INT[1], mServiceFake.screenBrightnessInt); + assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat)); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void testNoBrightnessDeliveredAfterFinish_Float() { mScreen.transitionTo(UNINITIALIZED, INITIALIZED); mScreen.transitionTo(INITIALIZED, DOZE_AOD); mScreen.transitionTo(DOZE_AOD, FINISH); @@ -440,11 +788,28 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { mSensor.sendSensorEvent(1); - assertNotEquals(1, mServiceFake.screenBrightness); + assertNotEquals(SENSOR_TO_BRIGHTNESS_FLOAT[1], mServiceFake.screenBrightnessFloat); + assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void testNonPositiveBrightness_keepsPreviousBrightnessAndScrim_Int() { + mScreen.transitionTo(UNINITIALIZED, INITIALIZED); + mScreen.transitionTo(INITIALIZED, DOZE_AOD); + waitForSensorManager(); + + mSensor.sendSensorEvent(1); + mSensor.sendSensorEvent(0); + + assertEquals(SENSOR_TO_BRIGHTNESS_INT[1], mServiceFake.screenBrightnessInt); + assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat)); + verify(mDozeHost).setAodDimmingScrim(eq(10f / 255f)); } @Test - public void testNonPositiveBrightness_keepsPreviousBrightnessAndScrim() { + @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void testNonPositiveBrightness_keepsPreviousBrightnessAndScrim_Float() { mScreen.transitionTo(UNINITIALIZED, INITIALIZED); mScreen.transitionTo(INITIALIZED, DOZE_AOD); waitForSensorManager(); @@ -452,7 +817,8 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { mSensor.sendSensorEvent(1); mSensor.sendSensorEvent(0); - assertEquals(1, mServiceFake.screenBrightness); + assertEquals(SENSOR_TO_BRIGHTNESS_FLOAT[1], mServiceFake.screenBrightnessFloat, DELTA); + assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt); verify(mDozeHost).setAodDimmingScrim(eq(10f / 255f)); } @@ -473,7 +839,8 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { } @Test - public void transitionToDoze_shouldClampBrightness_afterTimeout_clampsToDim() { + @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void transitionToDoze_shouldClampBrightness_afterTimeout_clampsToDim_Int() { when(mWakefulnessLifecycle.getLastSleepReason()).thenReturn( PowerManager.GO_TO_SLEEP_REASON_TIMEOUT); when(mDozeParameters.shouldClampToDimBrightness()).thenReturn(true); @@ -482,15 +849,57 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { // If we're dozing after a timeout, and playing the unlocked screen animation, we should // stay at or below dim brightness, because the screen dims just before timeout. - assertTrue(mServiceFake.screenBrightness <= DIM_BRIGHTNESS); + assertTrue(mServiceFake.screenBrightnessInt <= DIM_BRIGHTNESS_INT); + assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat)); // Once we transition to Doze, use the doze brightness mScreen.transitionTo(INITIALIZED, DOZE); - assertEquals(mServiceFake.screenBrightness, DEFAULT_BRIGHTNESS); + assertEquals(mServiceFake.screenBrightnessInt, DEFAULT_BRIGHTNESS_INT); + assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat)); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void transitionToDoze_shouldClampBrightness_afterTimeout_clampsToDim_Float() { + when(mWakefulnessLifecycle.getLastSleepReason()).thenReturn( + PowerManager.GO_TO_SLEEP_REASON_TIMEOUT); + when(mDozeParameters.shouldClampToDimBrightness()).thenReturn(true); + + mScreen.transitionTo(UNINITIALIZED, INITIALIZED); + + // If we're dozing after a timeout, and playing the unlocked screen animation, we should + // stay at or below dim brightness, because the screen dims just before timeout. + assertTrue(mServiceFake.screenBrightnessFloat <= DIM_BRIGHTNESS_FLOAT); + assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt); + + // Once we transition to Doze, use the doze brightness + mScreen.transitionTo(INITIALIZED, DOZE); + assertEquals(mServiceFake.screenBrightnessFloat, DEFAULT_BRIGHTNESS_FLOAT, DELTA); + assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void transitionToDoze_shouldClampBrightness_notAfterTimeout_doesNotClampToDim_Int() { + when(mWakefulnessLifecycle.getLastSleepReason()).thenReturn( + PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON); + when(mDozeParameters.shouldClampToDimBrightness()).thenReturn(true); + + mScreen.transitionTo(UNINITIALIZED, INITIALIZED); + + // If we're playing the unlocked screen off animation after a power button press, we should + // leave the brightness alone. + assertEquals(mServiceFake.screenBrightnessInt, DEFAULT_BRIGHTNESS_INT); + assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat)); + + mScreen.transitionTo(INITIALIZED, DOZE); + assertEquals(mServiceFake.screenBrightnessInt, DEFAULT_BRIGHTNESS_INT); + assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat)); } @Test - public void transitionToDoze_shouldClampBrightness_notAfterTimeout_doesNotClampToDim() { + @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void transitionToDoze_shouldClampBrightness_notAfterTimeout_doesNotClampToDim_Float() { when(mWakefulnessLifecycle.getLastSleepReason()).thenReturn( PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON); when(mDozeParameters.shouldClampToDimBrightness()).thenReturn(true); @@ -499,14 +908,32 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { // If we're playing the unlocked screen off animation after a power button press, we should // leave the brightness alone. - assertEquals(mServiceFake.screenBrightness, DEFAULT_BRIGHTNESS); + assertEquals(mServiceFake.screenBrightnessFloat, DEFAULT_BRIGHTNESS_FLOAT, DELTA); + assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt); + + mScreen.transitionTo(INITIALIZED, DOZE); + assertEquals(mServiceFake.screenBrightnessFloat, DEFAULT_BRIGHTNESS_FLOAT, DELTA); + assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void transitionToDoze_noClamp_afterTimeout_noScreenOff_doesNotClampToDim_Int() { + when(mWakefulnessLifecycle.getLastSleepReason()).thenReturn( + PowerManager.GO_TO_SLEEP_REASON_TIMEOUT); + when(mDozeParameters.shouldClampToDimBrightness()).thenReturn(false); + mScreen.transitionTo(UNINITIALIZED, INITIALIZED); mScreen.transitionTo(INITIALIZED, DOZE); - assertEquals(mServiceFake.screenBrightness, DEFAULT_BRIGHTNESS); + + // If we aren't controlling the screen off animation, we should leave the brightness alone. + assertEquals(mServiceFake.screenBrightnessInt, DEFAULT_BRIGHTNESS_INT); + assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat)); } @Test - public void transitionToDoze_noClampBrightness_afterTimeout_noScreenOff_doesNotClampToDim() { + @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void transitionToDoze_noClamp_afterTimeout_noScreenOff_doesNotClampToDim_Float() { when(mWakefulnessLifecycle.getLastSleepReason()).thenReturn( PowerManager.GO_TO_SLEEP_REASON_TIMEOUT); when(mDozeParameters.shouldClampToDimBrightness()).thenReturn(false); @@ -515,11 +942,13 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { mScreen.transitionTo(INITIALIZED, DOZE); // If we aren't controlling the screen off animation, we should leave the brightness alone. - assertEquals(mServiceFake.screenBrightness, DEFAULT_BRIGHTNESS); + assertEquals(mServiceFake.screenBrightnessFloat, DEFAULT_BRIGHTNESS_FLOAT, DELTA); + assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt); } @Test - public void transitionToDoze_noClampBrightness_afterTimeout_clampsToDim() { + @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void transitionToDoze_noClampBrightness_afterTimeout_clampsToDim_Int() { when(mWakefulnessLifecycle.getLastSleepReason()).thenReturn( PowerManager.GO_TO_SLEEP_REASON_TIMEOUT); when(mWakefulnessLifecycle.getWakefulness()).thenReturn( @@ -528,11 +957,28 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { mScreen.transitionTo(UNINITIALIZED, INITIALIZED); - assertTrue(mServiceFake.screenBrightness <= DIM_BRIGHTNESS); + assertTrue(mServiceFake.screenBrightnessInt <= DIM_BRIGHTNESS_INT); + assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat)); } @Test - public void transitionToDoze_noClampBrigthness_notAfterTimeout_doesNotClampToDim() { + @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void transitionToDoze_noClampBrightness_afterTimeout_clampsToDim_Float() { + when(mWakefulnessLifecycle.getLastSleepReason()).thenReturn( + PowerManager.GO_TO_SLEEP_REASON_TIMEOUT); + when(mWakefulnessLifecycle.getWakefulness()).thenReturn( + WakefulnessLifecycle.WAKEFULNESS_GOING_TO_SLEEP); + when(mDozeParameters.shouldClampToDimBrightness()).thenReturn(false); + + mScreen.transitionTo(UNINITIALIZED, INITIALIZED); + + assertTrue(mServiceFake.screenBrightnessFloat <= DIM_BRIGHTNESS_FLOAT); + assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void transitionToDoze_noClampBrigthness_notAfterTimeout_doesNotClampToDim_Int() { when(mWakefulnessLifecycle.getLastSleepReason()).thenReturn( PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON); when(mWakefulnessLifecycle.getWakefulness()).thenReturn( @@ -542,11 +988,47 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { mScreen.transitionTo(UNINITIALIZED, INITIALIZED); mScreen.transitionTo(INITIALIZED, DOZE); - assertEquals(mServiceFake.screenBrightness, DEFAULT_BRIGHTNESS); + assertEquals(mServiceFake.screenBrightnessInt, DEFAULT_BRIGHTNESS_INT); + assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat)); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void transitionToDoze_noClampBrigthness_notAfterTimeout_doesNotClampToDim_Float() { + when(mWakefulnessLifecycle.getLastSleepReason()).thenReturn( + PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON); + when(mWakefulnessLifecycle.getWakefulness()).thenReturn( + WakefulnessLifecycle.WAKEFULNESS_GOING_TO_SLEEP); + when(mDozeParameters.shouldClampToDimBrightness()).thenReturn(false); + + mScreen.transitionTo(UNINITIALIZED, INITIALIZED); + mScreen.transitionTo(INITIALIZED, DOZE); + + assertEquals(mServiceFake.screenBrightnessFloat, DEFAULT_BRIGHTNESS_FLOAT, DELTA); + assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void transitionToAodPaused_lightSensorDisabled_Int() { + // GIVEN AOD + mScreen.transitionTo(UNINITIALIZED, INITIALIZED); + mScreen.transitionTo(INITIALIZED, DOZE_AOD); + + // WHEN AOD is paused + mScreen.transitionTo(DOZE_AOD, DOZE_AOD_PAUSING); + mScreen.transitionTo(DOZE_AOD, DOZE_AOD_PAUSED); + waitForSensorManager(); + + // THEN new light events don't update brightness since the light sensor was unregistered + mSensor.sendSensorEvent(1); + assertEquals(mServiceFake.screenBrightnessInt, DEFAULT_BRIGHTNESS_INT); + assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat)); } @Test - public void transitionToAodPaused_lightSensorDisabled() { + @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void transitionToAodPaused_lightSensorDisabled_Float() { // GIVEN AOD mScreen.transitionTo(UNINITIALIZED, INITIALIZED); mScreen.transitionTo(INITIALIZED, DOZE_AOD); @@ -558,11 +1040,13 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { // THEN new light events don't update brightness since the light sensor was unregistered mSensor.sendSensorEvent(1); - assertEquals(mServiceFake.screenBrightness, DEFAULT_BRIGHTNESS); + assertEquals(mServiceFake.screenBrightnessFloat, DEFAULT_BRIGHTNESS_FLOAT, DELTA); + assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt); } @Test - public void transitionFromAodPausedToAod_lightSensorEnabled() { + @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void transitionFromAodPausedToAod_lightSensorEnabled_Int() { // GIVEN AOD paused mScreen.transitionTo(UNINITIALIZED, INITIALIZED); mScreen.transitionTo(INITIALIZED, DOZE_AOD); @@ -577,7 +1061,54 @@ public class DozeScreenBrightnessTest extends SysuiTestCase { mSensor.sendSensorEvent(1); // THEN aod brightness is updated - assertEquals(mServiceFake.screenBrightness, 1); + assertEquals(SENSOR_TO_BRIGHTNESS_INT[1], mServiceFake.screenBrightnessInt); + assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat)); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void transitionFromAodPausedToAod_lightSensorEnabled_Float() { + // GIVEN AOD paused + mScreen.transitionTo(UNINITIALIZED, INITIALIZED); + mScreen.transitionTo(INITIALIZED, DOZE_AOD); + mScreen.transitionTo(DOZE_AOD, DOZE_AOD_PAUSING); + mScreen.transitionTo(DOZE_AOD, DOZE_AOD_PAUSED); + + // WHEN device transitions back to AOD + mScreen.transitionTo(DOZE_AOD_PAUSED, DOZE_AOD); + waitForSensorManager(); + + // WHEN there are brightness changes + mSensor.sendSensorEvent(1); + + // THEN aod brightness is updated + assertEquals(SENSOR_TO_BRIGHTNESS_FLOAT[1], mServiceFake.screenBrightnessFloat, DELTA); + assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT) + public void fallBackToIntIfFloatBrightnessUndefined() { + when(mDisplayManager.getDozeBrightnessSensorValueToBrightness(Display.DEFAULT_DISPLAY)) + .thenReturn(null); + mScreen = new DozeScreenBrightness( + mContext, + mServiceFake, + mSensorManager, + new Optional[]{Optional.of(mSensor.getSensor())}, + mDozeHost, + null /* handler */, + mAlwaysOnDisplayPolicy, + mWakefulnessLifecycle, + mDozeParameters, + mDevicePostureController, + mDozeLog, + mSystemSettings, + mDisplayManager); + mScreen.transitionTo(UNINITIALIZED, INITIALIZED); + + assertEquals(DEFAULT_BRIGHTNESS_INT, mServiceFake.screenBrightnessInt); + assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat)); } private void waitForSensorManager() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeServiceFake.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeServiceFake.java index 928b314197b1..f55c2b77ca0f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeServiceFake.java +++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeServiceFake.java @@ -30,7 +30,8 @@ public class DozeServiceFake implements DozeMachine.Service { public int screenState; public boolean screenStateSet; public boolean requestedWakeup; - public int screenBrightness; + public int screenBrightnessInt; + public float screenBrightnessFloat; public DozeServiceFake() { reset(); @@ -54,7 +55,12 @@ public class DozeServiceFake implements DozeMachine.Service { @Override public void setDozeScreenBrightness(int brightness) { - screenBrightness = brightness; + screenBrightnessInt = brightness; + } + + @Override + public void setDozeScreenBrightnessFloat(float brightness) { + screenBrightnessFloat = brightness; } public void reset() { @@ -62,6 +68,7 @@ public class DozeServiceFake implements DozeMachine.Service { screenState = Display.STATE_UNKNOWN; screenStateSet = false; requestedWakeup = false; - screenBrightness = PowerManager.BRIGHTNESS_DEFAULT; + screenBrightnessInt = PowerManager.BRIGHTNESS_DEFAULT; + screenBrightnessFloat = PowerManager.BRIGHTNESS_INVALID_FLOAT; } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt index e01744e3576c..6a43a61dad77 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt @@ -20,6 +20,7 @@ import android.app.Dialog import android.content.ContextWrapper import android.content.SharedPreferences import android.os.Handler +import android.platform.test.annotations.DisableFlags import android.provider.Settings import android.provider.Settings.Global.ZEN_MODE_NO_INTERRUPTIONS import android.provider.Settings.Global.ZEN_MODE_OFF @@ -61,6 +62,7 @@ import org.mockito.Mockito.`when` as whenever @SmallTest @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper(setAsMainLooper = true) +@DisableFlags(android.app.Flags.FLAG_MODES_UI) class DndTileTest : SysuiTestCase() { companion object { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt index 05057278f7ca..689fc7cb647b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt @@ -28,7 +28,11 @@ import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardUpdateMonitor import com.android.server.notification.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING import com.android.systemui.SysuiTestCase +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor +import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.RankingBuilder import com.android.systemui.statusbar.StatusBarState @@ -45,6 +49,7 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController +import com.android.systemui.testKosmos import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq @@ -52,6 +57,7 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.withArgCaptor import dagger.BindsInstance import dagger.Component +import kotlinx.coroutines.CoroutineScope import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test @@ -64,6 +70,8 @@ import org.mockito.Mockito.`when` as whenever @RunWith(AndroidJUnit4::class) class SensitiveContentCoordinatorTest : SysuiTestCase() { + val kosmos = testKosmos() + val dynamicPrivacyController: DynamicPrivacyController = mock() val lockscreenUserManager: NotificationLockscreenUserManager = mock() val pipeline: NotifPipeline = mock() @@ -73,6 +81,8 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { val mSelectedUserInteractor: SelectedUserInteractor = mock() val sensitiveNotificationProtectionController: SensitiveNotificationProtectionController = mock() + val deviceEntryInteractor: DeviceEntryInteractor = mock() + val sceneInteractor: SceneInteractor = mock() val coordinator: SensitiveContentCoordinator = DaggerTestSensitiveContentCoordinatorComponent.factory() @@ -83,7 +93,10 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { statusBarStateController, keyguardStateController, mSelectedUserInteractor, - sensitiveNotificationProtectionController + sensitiveNotificationProtectionController, + deviceEntryInteractor, + sceneInteractor, + kosmos.applicationCoroutineScope, ) .coordinator @@ -136,8 +149,7 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { @Test @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) fun screenshareSecretFilter_sensitiveInctive_noFiltersSecret() { - whenever(sensitiveNotificationProtectionController.isSensitiveStateActive) - .thenReturn(false) + whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(false) coordinator.attach(pipeline) val filter = withArgCaptor<NotifFilter> { verify(pipeline).addFinalizeFilter(capture()) } @@ -683,10 +695,11 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { val mockSbn: StatusBarNotification = mock<StatusBarNotification>().apply { whenever(user).thenReturn(mockUserHandle) } val mockRow: ExpandableNotificationRow = mock<ExpandableNotificationRow>() - val mockEntry = mock<NotificationEntry>().apply { - whenever(sbn).thenReturn(mockSbn) - whenever(row).thenReturn(mockRow) - } + val mockEntry = + mock<NotificationEntry>().apply { + whenever(sbn).thenReturn(mockSbn) + whenever(row).thenReturn(mockRow) + } whenever(lockscreenUserManager.needsRedaction(mockEntry)).thenReturn(needsRedaction) whenever(mockEntry.rowExists()).thenReturn(true) return object : ListEntry("key", 0) { @@ -737,6 +750,9 @@ interface TestSensitiveContentCoordinatorComponent { @BindsInstance selectedUserInteractor: SelectedUserInteractor, @BindsInstance sensitiveNotificationProtectionController: SensitiveNotificationProtectionController, + @BindsInstance deviceEntryInteractor: DeviceEntryInteractor, + @BindsInstance sceneInteractor: SceneInteractor, + @BindsInstance @Application scope: CoroutineScope, ): TestSensitiveContentCoordinatorComponent } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index af5e60e9cd01..9b611057c059 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -1068,7 +1068,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { public void testShowBouncerOrKeyguard_needsFullScreen() { when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn( KeyguardSecurityModel.SecurityMode.SimPin); - mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false); + mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false); verify(mCentralSurfaces).hideKeyguard(); verify(mPrimaryBouncerInteractor).show(true); } @@ -1084,7 +1084,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { .thenReturn(KeyguardState.LOCKSCREEN); reset(mCentralSurfaces); - mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false); + mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false); verify(mPrimaryBouncerInteractor).show(true); verify(mCentralSurfaces).showKeyguard(); } @@ -1092,11 +1092,26 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Test @DisableSceneContainer public void testShowBouncerOrKeyguard_needsFullScreen_bouncerAlreadyShowing() { + boolean isFalsingReset = false; when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn( KeyguardSecurityModel.SecurityMode.SimPin); when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true); - mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false); + mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset); verify(mCentralSurfaces, never()).hideKeyguard(); + verify(mPrimaryBouncerInteractor).show(true); + } + + @Test + @DisableSceneContainer + public void testShowBouncerOrKeyguard_needsFullScreen_bouncerAlreadyShowing_onFalsing() { + boolean isFalsingReset = true; + when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn( + KeyguardSecurityModel.SecurityMode.SimPin); + when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true); + mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset); + verify(mCentralSurfaces, never()).hideKeyguard(); + + // Do not refresh the full screen bouncer if the call is from falsing verify(mPrimaryBouncerInteractor, never()).show(true); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt index 73ac6e3573d6..af4f647923a7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt @@ -22,6 +22,7 @@ import android.telephony.TelephonyCallback import android.telephony.TelephonyManager import android.telephony.satellite.NtnSignalStrength import android.telephony.satellite.NtnSignalStrengthCallback +import android.telephony.satellite.SatelliteCommunicationAllowedStateCallback import android.telephony.satellite.SatelliteManager import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_CONNECTED import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_DATAGRAM_RETRYING @@ -44,7 +45,6 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.log.core.FakeLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileTelephonyHelpers import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.MIN_UPTIME -import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.POLLING_INTERVAL_MS import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever @@ -54,11 +54,8 @@ import com.google.common.truth.Truth.assertThat import java.util.Optional import kotlin.test.Test import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -71,6 +68,7 @@ import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import org.mockito.kotlin.doThrow @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @OptIn(ExperimentalCoroutinesApi::class) @@ -186,149 +184,83 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { } @Test - fun isSatelliteAllowed_readsSatelliteManagerState_enabled() = + fun isSatelliteAllowed_listensToSatelliteManagerCallback() = testScope.runTest { setupDefaultRepo() - // GIVEN satellite is allowed in this location - val allowed = true - - doAnswer { - val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException> - receiver.onResult(allowed) - null - } - .`when`(satelliteManager) - .requestIsCommunicationAllowedForCurrentLocation( - any(), - any<OutcomeReceiver<Boolean, SatelliteException>>() - ) val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation) + runCurrent() - assertThat(latest).isTrue() - } - - @Test - fun isSatelliteAllowed_readsSatelliteManagerState_disabled() = - testScope.runTest { - setupDefaultRepo() - // GIVEN satellite is not allowed in this location - val allowed = false - - doAnswer { - val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException> - receiver.onResult(allowed) - null + val callback = + withArgCaptor<SatelliteCommunicationAllowedStateCallback> { + verify(satelliteManager) + .registerForCommunicationAllowedStateChanged(any(), capture()) } - .`when`(satelliteManager) - .requestIsCommunicationAllowedForCurrentLocation( - any(), - any<OutcomeReceiver<Boolean, SatelliteException>>() - ) - val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation) + // WHEN satellite manager says it's not available + callback.onSatelliteCommunicationAllowedStateChanged(false) + // THEN it's not! assertThat(latest).isFalse() + + // WHEN satellite manager says it's changed to available + callback.onSatelliteCommunicationAllowedStateChanged(true) + + // THEN it is! + assertThat(latest).isTrue() } @Test - fun isSatelliteAllowed_pollsOnTimeout() = + fun isSatelliteAllowed_falseWhenErrorOccurs() = testScope.runTest { setupDefaultRepo() - // GIVEN satellite is not allowed in this location - var allowed = false - doAnswer { - val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException> - receiver.onResult(allowed) - null - } + // GIVEN SatelliteManager gon' throw exceptions when we ask to register the callback + doThrow(RuntimeException("Test exception")) .`when`(satelliteManager) - .requestIsCommunicationAllowedForCurrentLocation( - any(), - any<OutcomeReceiver<Boolean, SatelliteException>>() - ) + .registerForCommunicationAllowedStateChanged(any(), any()) + // WHEN the latest value is requested (and thus causes an exception to be thrown) val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation) + // THEN the value is just false, and we didn't crash! assertThat(latest).isFalse() - - // WHEN satellite becomes enabled - allowed = true - - // WHEN the timeout has not yet been reached - advanceTimeBy(POLLING_INTERVAL_MS / 2) - - // THEN the value is still false - assertThat(latest).isFalse() - - // WHEN time advances beyond the polling interval - advanceTimeBy(POLLING_INTERVAL_MS / 2 + 1) - - // THEN then new value is emitted - assertThat(latest).isTrue() } @Test - fun isSatelliteAllowed_pollingRestartsWhenCollectionRestarts() = + fun isSatelliteAllowed_reRegistersOnTelephonyProcessCrash() = testScope.runTest { setupDefaultRepo() - // Use the old school launch/cancel so we can simulate subscribers arriving and leaving - - var latest: Boolean? = false - var job = - underTest.isSatelliteAllowedForCurrentLocation.onEach { latest = it }.launchIn(this) - - // GIVEN satellite is not allowed in this location - var allowed = false + val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation) + runCurrent() - doAnswer { - val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException> - receiver.onResult(allowed) - null + val callback = + withArgCaptor<SatelliteCommunicationAllowedStateCallback> { + verify(satelliteManager) + .registerForCommunicationAllowedStateChanged(any(), capture()) } - .`when`(satelliteManager) - .requestIsCommunicationAllowedForCurrentLocation( - any(), - any<OutcomeReceiver<Boolean, SatelliteException>>() - ) - assertThat(latest).isFalse() - - // WHEN satellite becomes enabled - allowed = true - - // WHEN the job is restarted - advanceTimeBy(POLLING_INTERVAL_MS / 2) + val telephonyCallback = + MobileTelephonyHelpers.getTelephonyCallbackForType< + TelephonyCallback.RadioPowerStateListener + >( + telephonyManager + ) - job.cancel() - job = - underTest.isSatelliteAllowedForCurrentLocation.onEach { latest = it }.launchIn(this) + // GIVEN satellite is currently provisioned + callback.onSatelliteCommunicationAllowedStateChanged(true) - // THEN the value is re-fetched assertThat(latest).isTrue() - job.cancel() - } - - @Test - fun isSatelliteAllowed_falseWhenErrorOccurs() = - testScope.runTest { - setupDefaultRepo() - doAnswer { - val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException> - receiver.onError(SatelliteException(1 /* unused */)) - null - } - .`when`(satelliteManager) - .requestIsCommunicationAllowedForCurrentLocation( - any(), - any<OutcomeReceiver<Boolean, SatelliteException>>() - ) - - val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation) + // WHEN a crash event happens (detected by radio state change) + telephonyCallback.onRadioPowerStateChanged(TelephonyManager.RADIO_POWER_ON) + runCurrent() + telephonyCallback.onRadioPowerStateChanged(TelephonyManager.RADIO_POWER_OFF) + runCurrent() - assertThat(latest).isFalse() + // THEN listener is re-registered + verify(satelliteManager, times(2)) + .registerForCommunicationAllowedStateChanged(any(), any()) } @Test @@ -363,24 +295,21 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { testScope.runTest { // GIVEN satellite is supported on device doAnswer { - val callback: OutcomeReceiver<Boolean, SatelliteException> = - it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException> - callback.onResult(true) - } + val callback: OutcomeReceiver<Boolean, SatelliteException> = + it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException> + callback.onResult(true) + } .whenever(satelliteManager) .requestIsSupported(any(), any()) // GIVEN satellite returns an error when asked if provisioned doAnswer { - val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException> - receiver.onError(SatelliteException(SATELLITE_RESULT_ERROR)) - null - } + val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException> + receiver.onError(SatelliteException(SATELLITE_RESULT_ERROR)) + null + } .whenever(satelliteManager) - .requestIsProvisioned( - any(), - any<OutcomeReceiver<Boolean, SatelliteException>>() - ) + .requestIsProvisioned(any(), any<OutcomeReceiver<Boolean, SatelliteException>>()) // GIVEN we've been up long enough to start querying systemClock.setUptimeMillis(Process.getStartUptimeMillis() + MIN_UPTIME) @@ -409,10 +338,10 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { testScope.runTest { // GIVEN satellite is supported on device doAnswer { - val callback: OutcomeReceiver<Boolean, SatelliteException> = - it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException> - callback.onResult(true) - } + val callback: OutcomeReceiver<Boolean, SatelliteException> = + it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException> + callback.onResult(true) + } .whenever(satelliteManager) .requestIsSupported(any(), any()) @@ -779,10 +708,10 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { .requestIsSupported(any(), any()) doAnswer { - val callback: OutcomeReceiver<Boolean, SatelliteException> = - it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException> - callback.onResult(initialSatelliteIsProvisioned) - } + val callback: OutcomeReceiver<Boolean, SatelliteException> = + it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException> + callback.onResult(initialSatelliteIsProvisioned) + } .whenever(satelliteManager) .requestIsProvisioned(any(), any()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/data/repository/TouchpadRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/data/repository/TouchpadRepositoryTest.kt new file mode 100644 index 000000000000..3783af554969 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/data/repository/TouchpadRepositoryTest.kt @@ -0,0 +1,184 @@ +/* + * 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.touchpad.data.repository + +import android.hardware.input.FakeInputManager +import android.hardware.input.InputManager.InputDeviceListener +import android.hardware.input.fakeInputManager +import android.testing.TestableLooper +import android.view.InputDevice +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.inputdevice.data.repository.InputDeviceRepository +import com.android.systemui.testKosmos +import com.android.systemui.utils.os.FakeHandler +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.eq +import org.mockito.Captor +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.whenever + +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidJUnit4::class) +class TouchpadRepositoryTest : SysuiTestCase() { + + @Captor private lateinit var deviceListenerCaptor: ArgumentCaptor<InputDeviceListener> + private lateinit var fakeInputManager: FakeInputManager + + private lateinit var underTest: TouchpadRepository + private lateinit var dispatcher: CoroutineDispatcher + private lateinit var inputDeviceRepo: InputDeviceRepository + private lateinit var testScope: TestScope + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + fakeInputManager = testKosmos().fakeInputManager + dispatcher = StandardTestDispatcher() + testScope = TestScope(dispatcher) + val handler = FakeHandler(TestableLooper.get(this).looper) + inputDeviceRepo = + InputDeviceRepository(handler, testScope.backgroundScope, fakeInputManager.inputManager) + underTest = + TouchpadRepositoryImpl(dispatcher, fakeInputManager.inputManager, inputDeviceRepo) + } + + @Test + fun emitsDisconnected_ifNothingIsConnected() = + testScope.runTest { + val initialState = underTest.isAnyTouchpadConnected.first() + assertThat(initialState).isFalse() + } + + @Test + fun emitsConnected_ifTouchpadAlreadyConnectedAtTheStart() = + testScope.runTest { + fakeInputManager.addDevice(TOUCHPAD_ID, TOUCHPAD) + val initialValue = underTest.isAnyTouchpadConnected.first() + assertThat(initialValue).isTrue() + } + + @Test + fun emitsConnected_whenNewTouchpadConnects() = + testScope.runTest { + captureDeviceListener() + val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected) + + fakeInputManager.addDevice(TOUCHPAD_ID, TOUCHPAD) + + assertThat(isTouchpadConnected).isTrue() + } + + @Test + fun emitsDisconnected_whenDeviceWithIdDoesNotExist() = + testScope.runTest { + captureDeviceListener() + val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected) + whenever(fakeInputManager.inputManager.getInputDevice(eq(NULL_DEVICE_ID))) + .thenReturn(null) + fakeInputManager.addDevice(NULL_DEVICE_ID, InputDevice.SOURCE_UNKNOWN) + assertThat(isTouchpadConnected).isFalse() + } + + @Test + fun emitsDisconnected_whenTouchpadDisconnects() = + testScope.runTest { + captureDeviceListener() + val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected) + + fakeInputManager.addDevice(TOUCHPAD_ID, TOUCHPAD) + assertThat(isTouchpadConnected).isTrue() + + fakeInputManager.removeDevice(TOUCHPAD_ID) + assertThat(isTouchpadConnected).isFalse() + } + + private suspend fun captureDeviceListener() { + underTest.isAnyTouchpadConnected.first() + Mockito.verify(fakeInputManager.inputManager) + .registerInputDeviceListener(deviceListenerCaptor.capture(), anyOrNull()) + fakeInputManager.registerInputDeviceListener(deviceListenerCaptor.value) + } + + @Test + fun emitsDisconnected_whenNonTouchpadConnects() = + testScope.runTest { + captureDeviceListener() + val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected) + + fakeInputManager.addDevice(NON_TOUCHPAD_ID, InputDevice.SOURCE_KEYBOARD) + assertThat(isTouchpadConnected).isFalse() + } + + @Test + fun emitsDisconnected_whenTouchpadDisconnectsAndWasAlreadyConnectedAtTheStart() = + testScope.runTest { + captureDeviceListener() + val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected) + + fakeInputManager.removeDevice(TOUCHPAD_ID) + assertThat(isTouchpadConnected).isFalse() + } + + @Test + fun emitsConnected_whenAnotherDeviceDisconnects() = + testScope.runTest { + captureDeviceListener() + val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected) + + fakeInputManager.addDevice(TOUCHPAD_ID, TOUCHPAD) + fakeInputManager.removeDevice(NON_TOUCHPAD_ID) + + assertThat(isTouchpadConnected).isTrue() + } + + @Test + fun emitsConnected_whenOneTouchpadDisconnectsButAnotherRemainsConnected() = + testScope.runTest { + captureDeviceListener() + val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected) + + fakeInputManager.addDevice(TOUCHPAD_ID, TOUCHPAD) + fakeInputManager.addDevice(ANOTHER_TOUCHPAD_ID, TOUCHPAD) + fakeInputManager.removeDevice(TOUCHPAD_ID) + + assertThat(isTouchpadConnected).isTrue() + } + + private companion object { + private const val TOUCHPAD_ID = 1 + private const val NON_TOUCHPAD_ID = 2 + private const val ANOTHER_TOUCHPAD_ID = 3 + private const val NULL_DEVICE_ID = 4 + + private const val TOUCHPAD = InputDevice.SOURCE_TOUCHPAD + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt index cf0db7b51676..8875b84055d8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt @@ -28,6 +28,9 @@ import android.view.MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.IN_PROGRESS +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NOT_STARTED import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith @@ -36,16 +39,19 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class BackGestureMonitorTest : SysuiTestCase() { - private var gestureDoneWasCalled = false - private val gestureDoneCallback = { gestureDoneWasCalled = true } - private val gestureMonitor = BackGestureMonitor(SWIPE_DISTANCE.toInt(), gestureDoneCallback) + private var gestureState = NOT_STARTED + private val gestureMonitor = + BackGestureMonitor( + gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(), + gestureStateChangedCallback = { gestureState = it } + ) companion object { const val SWIPE_DISTANCE = 100f } @Test - fun triggersGestureDoneForThreeFingerGestureRight() { + fun triggersGestureFinishedForThreeFingerGestureRight() { val events = listOf( threeFingerEvent(ACTION_DOWN, x = 0f, y = 0f), @@ -59,11 +65,11 @@ class BackGestureMonitorTest : SysuiTestCase() { events.forEach { gestureMonitor.processTouchpadEvent(it) } - assertThat(gestureDoneWasCalled).isTrue() + assertThat(gestureState).isEqualTo(FINISHED) } @Test - fun triggersGestureDoneForThreeFingerGestureLeft() { + fun triggersGestureFinishedForThreeFingerGestureLeft() { val events = listOf( threeFingerEvent(ACTION_DOWN, x = SWIPE_DISTANCE, y = 0f), @@ -77,7 +83,21 @@ class BackGestureMonitorTest : SysuiTestCase() { events.forEach { gestureMonitor.processTouchpadEvent(it) } - assertThat(gestureDoneWasCalled).isTrue() + assertThat(gestureState).isEqualTo(FINISHED) + } + + @Test + fun triggersGestureProgressForThreeFingerGestureStarted() { + val events = + listOf( + threeFingerEvent(ACTION_DOWN, x = SWIPE_DISTANCE, y = 0f), + threeFingerEvent(ACTION_POINTER_DOWN, x = SWIPE_DISTANCE, y = 0f), + threeFingerEvent(ACTION_POINTER_DOWN, x = SWIPE_DISTANCE, y = 0f), + ) + + events.forEach { gestureMonitor.processTouchpadEvent(it) } + + assertThat(gestureState).isEqualTo(IN_PROGRESS) } private fun threeFingerEvent(action: Int, x: Float, y: Float): MotionEvent { @@ -91,7 +111,7 @@ class BackGestureMonitorTest : SysuiTestCase() { } @Test - fun doesntTriggerGestureDone_onThreeFingersSwipeUp() { + fun doesntTriggerGestureFinished_onThreeFingersSwipeUp() { val events = listOf( threeFingerEvent(ACTION_DOWN, x = 0f, y = 0f), @@ -105,11 +125,11 @@ class BackGestureMonitorTest : SysuiTestCase() { events.forEach { gestureMonitor.processTouchpadEvent(it) } - assertThat(gestureDoneWasCalled).isFalse() + assertThat(gestureState).isEqualTo(NOT_STARTED) } @Test - fun doesntTriggerGestureDone_onTwoFingersSwipe() { + fun doesntTriggerGestureFinished_onTwoFingersSwipe() { fun twoFingerEvent(action: Int, x: Float, y: Float) = motionEvent( action = action, @@ -127,11 +147,11 @@ class BackGestureMonitorTest : SysuiTestCase() { events.forEach { gestureMonitor.processTouchpadEvent(it) } - assertThat(gestureDoneWasCalled).isFalse() + assertThat(gestureState).isEqualTo(NOT_STARTED) } @Test - fun doesntTriggerGestureDone_onFourFingersSwipe() { + fun doesntTriggerGestureFinished_onFourFingersSwipe() { fun fourFingerEvent(action: Int, x: Float, y: Float) = motionEvent( action = action, @@ -155,6 +175,6 @@ class BackGestureMonitorTest : SysuiTestCase() { events.forEach { gestureMonitor.processTouchpadEvent(it) } - assertThat(gestureDoneWasCalled).isFalse() + assertThat(gestureState).isEqualTo(NOT_STARTED) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt index 769f264f0870..dc4d5f674479 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt @@ -32,6 +32,8 @@ import android.view.MotionEvent.TOOL_TYPE_MOUSE import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NOT_STARTED import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGesture.BACK import com.google.common.truth.Truth.assertThat import org.junit.Test @@ -41,8 +43,8 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class TouchpadGestureHandlerTest : SysuiTestCase() { - private var gestureDone = false - private val handler = TouchpadGestureHandler(BACK, SWIPE_DISTANCE) { gestureDone = true } + private var gestureState = NOT_STARTED + private val handler = TouchpadGestureHandler(BACK, SWIPE_DISTANCE) { gestureState = it } companion object { const val SWIPE_DISTANCE = 100 @@ -84,7 +86,7 @@ class TouchpadGestureHandlerTest : SysuiTestCase() { fun triggersGestureDoneForThreeFingerGesture() { backGestureEvents().forEach { handler.onMotionEvent(it) } - assertThat(gestureDone).isTrue() + assertThat(gestureState).isEqualTo(FINISHED) } private fun backGestureEvents(): List<MotionEvent> { diff --git a/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt b/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt index 6e7c05ca3f8f..ee36cadd8480 100644 --- a/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt +++ b/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt @@ -16,6 +16,7 @@ package android.hardware.input +import android.hardware.input.InputManager.InputDeviceListener import android.view.InputDevice import android.view.KeyCharacterMap import android.view.KeyCharacterMap.VIRTUAL_KEYBOARD @@ -47,6 +48,8 @@ class FakeInputManager { VIRTUAL_KEYBOARD to allKeyCodes.toMutableSet() ) + private var inputDeviceListener: InputDeviceListener? = null + val inputManager = mock<InputManager> { whenever(getInputDevice(anyInt())).thenAnswer { invocation -> @@ -84,6 +87,11 @@ class FakeInputManager { addPhysicalKeyboard(deviceId, enabled) } + fun registerInputDeviceListener(listener: InputDeviceListener) { + // TODO (b/355422259): handle this by listening to inputManager.registerInputDeviceListener + inputDeviceListener = listener + } + fun addPhysicalKeyboard(id: Int, enabled: Boolean = true) { check(id > 0) { "Physical keyboard ids have to be > 0" } addKeyboard(id, enabled) @@ -106,6 +114,16 @@ class FakeInputManager { supportedKeyCodesByDeviceId[id] = allKeyCodes.toMutableSet() } + fun addDevice(id: Int, sources: Int) { + devices[id] = InputDevice.Builder().setId(id).setSources(sources).build() + inputDeviceListener?.onInputDeviceAdded(id) + } + + fun removeDevice(id: Int) { + devices.remove(id) + inputDeviceListener?.onInputDeviceRemoved(id) + } + private fun InputDevice.copy( id: Int = getId(), type: Int = keyboardType, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt index 2ca338a3af9c..f3d5b7d77669 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt @@ -33,7 +33,7 @@ class FakeUserTracker( private var _userHandle: UserHandle = UserHandle.of(_userId), private var _userInfo: UserInfo = mock(), private var _userProfiles: List<UserInfo> = emptyList(), - userContentResolver: ContentResolver = MockContentResolver(), + userContentResolverProvider: () -> ContentResolver = { MockContentResolver() }, userContext: Context = mock(), private val onCreateCurrentUserContext: (Context) -> Context = { mock() }, ) : UserTracker { @@ -41,14 +41,19 @@ class FakeUserTracker( override val userId: Int get() = _userId + override val userHandle: UserHandle get() = _userHandle + override val userInfo: UserInfo get() = _userInfo + override val userProfiles: List<UserInfo> get() = _userProfiles - override val userContentResolver: ContentResolver = userContentResolver + // userContentResolver is lazy because Ravenwood doesn't support MockContentResolver() + // and we still want to allow people use this class for tests that don't use it. + override val userContentResolver: ContentResolver by lazy { userContentResolverProvider() } override val userContext: Context = userContext override fun addCallback(callback: UserTracker.Callback, executor: Executor) { diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java index 8fe6853abb45..1a15d7a8c19e 100644 --- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java +++ b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java @@ -25,19 +25,24 @@ import static android.os.ParcelFileDescriptor.MODE_WORLD_READABLE; import static android.os.ParcelFileDescriptor.MODE_WORLD_WRITEABLE; import static android.os.ParcelFileDescriptor.MODE_WRITE_ONLY; +import android.system.ErrnoException; +import android.system.Os; +import android.util.Log; + import com.android.internal.annotations.GuardedBy; import com.android.ravenwood.common.JvmWorkaround; import java.io.File; import java.io.FileDescriptor; import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.io.IOException; import java.io.RandomAccessFile; import java.util.HashMap; import java.util.Map; public class ParcelFileDescriptor_host { + private static final String TAG = "ParcelFileDescriptor_host"; + /** * Since we don't have a great way to keep an unmanaged {@code FileDescriptor} reference * alive, we keep a strong reference to the {@code RandomAccessFile} we used to open it. This @@ -98,16 +103,18 @@ public class ParcelFileDescriptor_host { synchronized (sActive) { raf = sActive.remove(fd); } + int fdInt = JvmWorkaround.getInstance().getFdInt(fd); try { if (raf != null) { raf.close(); } else { - // Odd, we don't remember opening this ourselves, but let's release the - // underlying resource as requested - System.err.println("Closing unknown FileDescriptor: " + fd); - new FileOutputStream(fd).close(); + // This FD wasn't created by native_open$ravenwood(). + // The FD was passed to the PFD ctor. Just close it. + Os.close(fd); } - } catch (IOException ignored) { + } catch (IOException | ErrnoException e) { + Log.w(TAG, "Exception thrown while closing fd " + fdInt, e); } } } +;
\ No newline at end of file diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java index 22e11e18ae31..2df93cd93935 100644 --- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java +++ b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java @@ -15,6 +15,11 @@ */ package com.android.platform.test.ravenwood.nativesubstitution; +import android.system.ErrnoException; +import android.system.Os; +import android.util.Log; + +import java.io.FileDescriptor; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.Arrays; @@ -31,6 +36,8 @@ import java.util.concurrent.atomic.AtomicLong; * {@link ByteBuffer} wouldn't allow...) */ public class Parcel_host { + private static final String TAG = "Parcel"; + private Parcel_host() { } @@ -50,6 +57,11 @@ public class Parcel_host { // TODO Use the actual value from Parcel.java. private static final int OK = 0; + private final Map<Integer, FileDescriptor> mFdMap = new ConcurrentHashMap<>(); + + private static final int FD_PLACEHOLDER = 0xDEADBEEF; + private static final int FD_PAYLOAD_SIZE = 8; + private void validate() { if (mDeleted) { // TODO: Put more info @@ -67,6 +79,7 @@ public class Parcel_host { return p; } + /** Native method substitution */ public static long nativeCreate() { final long id = sNextId.getAndIncrement(); final Parcel_host p = new Parcel_host(); @@ -80,7 +93,8 @@ public class Parcel_host { mSize = 0; mPos = 0; mSensitive = false; - mAllowFds = false; + mAllowFds = true; + mFdMap.clear(); } private void updateSize() { @@ -89,16 +103,19 @@ public class Parcel_host { } } + /** Native method substitution */ public static void nativeDestroy(long nativePtr) { getInstance(nativePtr).mDeleted = true; sInstances.remove(nativePtr); } + /** Native method substitution */ public static void nativeFreeBuffer(long nativePtr) { getInstance(nativePtr).freeBuffer(); } - public void freeBuffer() { + /** Native method substitution */ + private void freeBuffer() { init(); } @@ -137,32 +154,47 @@ public class Parcel_host { } } + /** Native method substitution */ public static void nativeMarkSensitive(long nativePtr) { getInstance(nativePtr).mSensitive = true; } + + /** Native method substitution */ public static int nativeDataSize(long nativePtr) { return getInstance(nativePtr).mSize; } + + /** Native method substitution */ public static int nativeDataAvail(long nativePtr) { var p = getInstance(nativePtr); return p.mSize - p.mPos; } + + /** Native method substitution */ public static int nativeDataPosition(long nativePtr) { return getInstance(nativePtr).mPos; } + + /** Native method substitution */ public static int nativeDataCapacity(long nativePtr) { return getInstance(nativePtr).mBuffer.length; } + + /** Native method substitution */ public static void nativeSetDataSize(long nativePtr, int size) { var p = getInstance(nativePtr); p.ensureCapacity(size); getInstance(nativePtr).mSize = size; } + + /** Native method substitution */ public static void nativeSetDataPosition(long nativePtr, int pos) { var p = getInstance(nativePtr); // TODO: Should this change the size or the capacity?? p.mPos = pos; } + + /** Native method substitution */ public static void nativeSetDataCapacity(long nativePtr, int size) { if (size < 0) { throw new IllegalArgumentException("size < 0: size=" + size); @@ -173,20 +205,25 @@ public class Parcel_host { } } + /** Native method substitution */ public static boolean nativePushAllowFds(long nativePtr, boolean allowFds) { var p = getInstance(nativePtr); var prev = p.mAllowFds; p.mAllowFds = allowFds; return prev; } + + /** Native method substitution */ public static void nativeRestoreAllowFds(long nativePtr, boolean lastValue) { getInstance(nativePtr).mAllowFds = lastValue; } + /** Native method substitution */ public static void nativeWriteByteArray(long nativePtr, byte[] b, int offset, int len) { nativeWriteBlob(nativePtr, b, offset, len); } + /** Native method substitution */ public static void nativeWriteBlob(long nativePtr, byte[] b, int offset, int len) { var p = getInstance(nativePtr); @@ -205,6 +242,7 @@ public class Parcel_host { } } + /** Native method substitution */ public static int nativeWriteInt(long nativePtr, int value) { var p = getInstance(nativePtr); p.ensureMoreCapacity(Integer.BYTES); @@ -219,14 +257,19 @@ public class Parcel_host { return OK; } + /** Native method substitution */ public static int nativeWriteLong(long nativePtr, long value) { nativeWriteInt(nativePtr, (int) (value >>> 32)); nativeWriteInt(nativePtr, (int) (value)); return OK; } + + /** Native method substitution */ public static int nativeWriteFloat(long nativePtr, float val) { return nativeWriteInt(nativePtr, Float.floatToIntBits(val)); } + + /** Native method substitution */ public static int nativeWriteDouble(long nativePtr, double val) { return nativeWriteLong(nativePtr, Double.doubleToLongBits(val)); } @@ -235,6 +278,7 @@ public class Parcel_host { return ((val + 3) / 4) * 4; } + /** Native method substitution */ public static void nativeWriteString8(long nativePtr, String val) { if (val == null) { nativeWriteBlob(nativePtr, null, 0, 0); @@ -243,15 +287,19 @@ public class Parcel_host { nativeWriteBlob(nativePtr, bytes, 0, bytes.length); } } + + /** Native method substitution */ public static void nativeWriteString16(long nativePtr, String val) { // Just reuse String8 nativeWriteString8(nativePtr, val); } + /** Native method substitution */ public static byte[] nativeCreateByteArray(long nativePtr) { return nativeReadBlob(nativePtr); } + /** Native method substitution */ public static boolean nativeReadByteArray(long nativePtr, byte[] dest, int destLen) { if (dest == null) { return false; @@ -271,6 +319,7 @@ public class Parcel_host { return true; } + /** Native method substitution */ public static byte[] nativeReadBlob(long nativePtr) { var p = getInstance(nativePtr); if (p.mSize - p.mPos < 4) { @@ -295,6 +344,8 @@ public class Parcel_host { return bytes; } + + /** Native method substitution */ public static int nativeReadInt(long nativePtr) { var p = getInstance(nativePtr); @@ -310,19 +361,24 @@ public class Parcel_host { return ret; } + + /** Native method substitution */ public static long nativeReadLong(long nativePtr) { return (((long) nativeReadInt(nativePtr)) << 32) | (((long) nativeReadInt(nativePtr)) & 0xffff_ffffL); } + /** Native method substitution */ public static float nativeReadFloat(long nativePtr) { return Float.intBitsToFloat(nativeReadInt(nativePtr)); } + /** Native method substitution */ public static double nativeReadDouble(long nativePtr) { return Double.longBitsToDouble(nativeReadLong(nativePtr)); } + /** Native method substitution */ public static String nativeReadString8(long nativePtr) { final var bytes = nativeReadBlob(nativePtr); if (bytes == null) { @@ -334,10 +390,13 @@ public class Parcel_host { return nativeReadString8(nativePtr); } + /** Native method substitution */ public static byte[] nativeMarshall(long nativePtr) { var p = getInstance(nativePtr); return Arrays.copyOf(p.mBuffer, p.mSize); } + + /** Native method substitution */ public static void nativeUnmarshall( long nativePtr, byte[] data, int offset, int length) { var p = getInstance(nativePtr); @@ -346,6 +405,8 @@ public class Parcel_host { p.mPos += length; p.updateSize(); } + + /** Native method substitution */ public static int nativeCompareData(long thisNativePtr, long otherNativePtr) { var a = getInstance(thisNativePtr); var b = getInstance(otherNativePtr); @@ -355,6 +416,8 @@ public class Parcel_host { return -1; } } + + /** Native method substitution */ public static boolean nativeCompareDataInRange( long ptrA, int offsetA, long ptrB, int offsetB, int length) { var a = getInstance(ptrA); @@ -368,6 +431,8 @@ public class Parcel_host { return Arrays.equals(Arrays.copyOfRange(a.mBuffer, offsetA, offsetA + length), Arrays.copyOfRange(b.mBuffer, offsetB, offsetB + length)); } + + /** Native method substitution */ public static void nativeAppendFrom( long thisNativePtr, long otherNativePtr, int srcOffset, int length) { var dst = getInstance(thisNativePtr); @@ -382,25 +447,83 @@ public class Parcel_host { // TODO: Update the other's position? } - public static boolean nativeHasFileDescriptors(long nativePtr) { - // Assume false for now, because we don't support writing FDs yet. + /** Native method substitution */ + public static boolean nativeHasBinders(long nativePtr) { + // Assume false for now, because we don't support adding binders. return false; } - public static boolean nativeHasFileDescriptorsInRange( + /** Native method substitution */ + public static boolean nativeHasBindersInRange( long nativePtr, int offset, int length) { // Assume false for now, because we don't support writing FDs yet. return false; } - public static boolean nativeHasBinders(long nativePtr) { - // Assume false for now, because we don't support adding binders. - return false; + /** Native method substitution */ + public static void nativeWriteFileDescriptor(long nativePtr, java.io.FileDescriptor val) { + var p = getInstance(nativePtr); + + if (!p.mAllowFds) { + // Simulate the FDS_NOT_ALLOWED case in frameworks/base/core/jni/android_util_Binder.cpp + throw new RuntimeException("Not allowed to write file descriptors here"); + } + + FileDescriptor dup = null; + try { + dup = Os.dup(val); + } catch (ErrnoException e) { + throw new RuntimeException(e); + } + p.mFdMap.put(p.mPos, dup); + + // Parcel.cpp writes two int32s for a FD. + // Make sure FD_PAYLOAD_SIZE is in sync with this code. + nativeWriteInt(nativePtr, FD_PLACEHOLDER); + nativeWriteInt(nativePtr, FD_PLACEHOLDER); } - public static boolean nativeHasBindersInRange( - long nativePtr, int offset, int length) { - // Assume false for now, because we don't support writing FDs yet. + /** Native method substitution */ + public static java.io.FileDescriptor nativeReadFileDescriptor(long nativePtr) { + var p = getInstance(nativePtr); + + var pos = p.mPos; + var fd = p.mFdMap.get(pos); + + if (fd == null) { + Log.w(TAG, "nativeReadFileDescriptor: Not a FD at pos #" + pos); + return null; + } + nativeReadInt(nativePtr); + return fd; + } + + /** Native method substitution */ + public static boolean nativeHasFileDescriptors(long nativePtr) { + var p = getInstance(nativePtr); + return p.mFdMap.size() > 0; + } + + /** Native method substitution */ + public static boolean nativeHasFileDescriptorsInRange(long nativePtr, int offset, int length) { + var p = getInstance(nativePtr); + + // Original code: hasFileDescriptorsInRange() in frameworks/native/libs/binder/Parcel.cpp + if (offset < 0 || length < 0) { + throw new IllegalArgumentException("Negative value not allowed: offset=" + offset + + " length=" + length); + } + long limit = (long) offset + (long) length; + if (limit > p.mSize) { + throw new IllegalArgumentException("Out of range: offset=" + offset + + " length=" + length + " dataSize=" + p.mSize); + } + + for (var pos : p.mFdMap.keySet()) { + if (offset <= pos && (pos + FD_PAYLOAD_SIZE - 1) < (offset + length)) { + return true; + } + } return false; } -} +}
\ No newline at end of file diff --git a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java index 8a1fe62db4e1..825ab72e773a 100644 --- a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java +++ b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java @@ -53,4 +53,9 @@ public final class Os { public static StructStat stat(String path) throws ErrnoException { return RavenwoodRuntimeNative.stat(path); } + + /** Ravenwood version of the OS API. */ + public static void close(FileDescriptor fd) throws ErrnoException { + RavenwoodRuntimeNative.close(fd); + } } diff --git a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java index e9b305e5d789..2bc8e7123aad 100644 --- a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java +++ b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java @@ -48,6 +48,8 @@ public class RavenwoodRuntimeNative { public static native StructStat stat(String path) throws ErrnoException; + private static native void nClose(int fd) throws ErrnoException; + public static long lseek(FileDescriptor fd, long offset, int whence) throws ErrnoException { return nLseek(JvmWorkaround.getInstance().getFdInt(fd), offset, whence); } @@ -83,4 +85,11 @@ public class RavenwoodRuntimeNative { return nFstat(fdInt); } + + /** See close(2) */ + public static void close(FileDescriptor fd) throws ErrnoException { + var fdInt = JvmWorkaround.getInstance().getFdInt(fd); + + nClose(fdInt); + } } diff --git a/ravenwood/runtime-jni/ravenwood_runtime.cpp b/ravenwood/runtime-jni/ravenwood_runtime.cpp index e0a3e1c9edf6..ee84954a5c2a 100644 --- a/ravenwood/runtime-jni/ravenwood_runtime.cpp +++ b/ravenwood/runtime-jni/ravenwood_runtime.cpp @@ -167,6 +167,11 @@ static jobject Linux_stat(JNIEnv* env, jobject, jstring javaPath) { return doStat(env, javaPath, false); } +static void nClose(JNIEnv* env, jclass, jint fd) { + // Don't use TEMP_FAILURE_RETRY() on close(): https://lkml.org/lkml/2005/9/10/129 + throwIfMinusOne(env, "close", close(fd)); +} + // ---- Registration ---- static const JNINativeMethod sMethods[] = @@ -179,6 +184,7 @@ static const JNINativeMethod sMethods[] = { "nFstat", "(I)Landroid/system/StructStat;", (void*)nFstat }, { "lstat", "(Ljava/lang/String;)Landroid/system/StructStat;", (void*)Linux_lstat }, { "stat", "(Ljava/lang/String;)Landroid/system/StructStat;", (void*)Linux_stat }, + { "nClose", "(I)V", (void*)nClose }, }; extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 30c743e508b6..9067cda6086d 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -41,6 +41,7 @@ import static android.content.Context.DEVICE_ID_DEFAULT; import static android.provider.Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED; import static android.view.accessibility.AccessibilityManager.FlashNotificationReason; +import static com.android.hardware.input.Flags.keyboardA11yMouseKeys; import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME; import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_COMPONENT_NAME; import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; @@ -57,7 +58,6 @@ import static com.android.internal.accessibility.util.AccessibilityStatsLogUtils import static com.android.internal.util.FunctionalUtils.ignoreRemoteException; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; import static com.android.server.accessibility.AccessibilityUserState.doesShortcutTargetsStringContain; -import static com.android.hardware.input.Flags.keyboardA11yMouseKeys; import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import android.accessibilityservice.AccessibilityGestureEvent; @@ -825,25 +825,27 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub @VisibleForTesting boolean onPackagesForceStoppedLocked( String[] packages, AccessibilityUserState userState) { - final List<String> continuousServicePackages = + final Set<String> packageSet = new HashSet<>(List.of(packages)); + final ArrayList<ComponentName> continuousServices = new ArrayList<>( userState.mInstalledServices.stream().filter(service -> (service.flags & FLAG_REQUEST_ACCESSIBILITY_BUTTON) == FLAG_REQUEST_ACCESSIBILITY_BUTTON - ).map(service -> service.getComponentName().flattenToString()).toList(); + ).map(AccessibilityServiceInfo::getComponentName).toList()); + + // Filter out continuous packages that are not from the array of stopped packages. + continuousServices.removeIf( + continuousName -> !packageSet.contains(continuousName.getPackageName())); boolean enabledServicesChanged = false; final Iterator<ComponentName> it = userState.mEnabledServices.iterator(); while (it.hasNext()) { final ComponentName comp = it.next(); final String compPkg = comp.getPackageName(); - for (String pkg : packages) { - if (compPkg.equals(pkg)) { - it.remove(); - userState.getBindingServicesLocked().remove(comp); - userState.getCrashedServicesLocked().remove(comp); - enabledServicesChanged = true; - break; - } + if (packageSet.contains(compPkg)) { + it.remove(); + userState.getBindingServicesLocked().remove(comp); + userState.getCrashedServicesLocked().remove(comp); + enabledServicesChanged = true; } } if (enabledServicesChanged) { @@ -855,8 +857,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub // Remove any button targets that match any stopped continuous services Set<String> buttonTargets = userState.getShortcutTargetsLocked(SOFTWARE); boolean buttonTargetsChanged = buttonTargets.removeIf( - target -> continuousServicePackages.stream().anyMatch( - pkg -> Objects.equals(target, pkg))); + target -> continuousServices.stream().anyMatch( + continuousName -> continuousName.flattenToString().equals(target))); if (buttonTargetsChanged) { userState.updateShortcutTargetsLocked(buttonTargets, SOFTWARE); persistColonDelimitedSetToSettingLocked( @@ -2641,7 +2643,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } - private <T> void persistColonDelimitedSetToSettingLocked(String settingName, int userId, + @VisibleForTesting + <T> void persistColonDelimitedSetToSettingLocked(String settingName, int userId, Set<T> set, Function<T, String> toString) { persistColonDelimitedSetToSettingLocked(settingName, userId, set, toString, /* defaultEmptyString= */ null); diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java index e9c3fbdf021c..0ee58967c9b7 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java @@ -523,6 +523,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH mScaleGestureDetector = new ScaleGestureDetector(context, this, Handler.getMain()); mScaleGestureDetector.setQuickScaleEnabled(false); mScrollGestureDetector = new GestureDetector(context, this, Handler.getMain()); + mScrollGestureDetector.setIsLongpressEnabled(false); } @Override @@ -1658,11 +1659,12 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH } float dX = event.getX() - firstPointerDownLocation.x; float dY = event.getY() - firstPointerDownLocation.y; - if (isAtLeftEdge() && dX > 0) { + if (isAtLeftEdge() && isScrollingLeft(dX, dY)) { return OVERSCROLL_LEFT_EDGE; - } else if (isAtRightEdge() && dX < 0) { + } else if (isAtRightEdge() && isScrollingRight(dX, dY)) { return OVERSCROLL_RIGHT_EDGE; - } else if ((isAtTopEdge() && dY > 0) || (isAtBottomEdge() && dY < 0)) { + } else if ((isAtTopEdge() && isScrollingUp(dX, dY)) + || (isAtBottomEdge() && isScrollingDown(dX, dY))) { return OVERSCROLL_VERTICAL_EDGE; } return OVERSCROLL_NONE; @@ -1672,18 +1674,34 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH return mFullScreenMagnificationController.isAtLeftEdge(mDisplayId, mOverscrollEdgeSlop); } + private static boolean isScrollingLeft(float dX, float dY) { + return Math.abs(dX) > Math.abs(dY) && dX > 0; + } + private boolean isAtRightEdge() { return mFullScreenMagnificationController.isAtRightEdge(mDisplayId, mOverscrollEdgeSlop); } + private static boolean isScrollingRight(float dX, float dY) { + return Math.abs(dX) > Math.abs(dY) && dX < 0; + } + private boolean isAtTopEdge() { return mFullScreenMagnificationController.isAtTopEdge(mDisplayId, mOverscrollEdgeSlop); } + private static boolean isScrollingUp(float dX, float dY) { + return Math.abs(dX) < Math.abs(dY) && dY > 0; + } + private boolean isAtBottomEdge() { return mFullScreenMagnificationController.isAtBottomEdge(mDisplayId, mOverscrollEdgeSlop); } + private static boolean isScrollingDown(float dX, float dY) { + return Math.abs(dX) < Math.abs(dY) && dY < 0; + } + private boolean pointerValid(PointF pointerDownLocation) { return !(Float.isNaN(pointerDownLocation.x) && Float.isNaN(pointerDownLocation.y)); } @@ -1876,6 +1894,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH private MotionEventInfo mEvent; SinglePanningState(Context context) { mScrollGestureDetector = new GestureDetector(context, this, Handler.getMain()); + mScrollGestureDetector.setIsLongpressEnabled(false); } @Override diff --git a/services/core/java/com/android/server/am/PendingIntentController.java b/services/core/java/com/android/server/am/PendingIntentController.java index f3361203c44a..3b0147cb665d 100644 --- a/services/core/java/com/android/server/am/PendingIntentController.java +++ b/services/core/java/com/android/server/am/PendingIntentController.java @@ -149,21 +149,6 @@ public class PendingIntentController { ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED); } - if (opts != null && opts.isPendingIntentBackgroundActivityLaunchAllowedByPermission()) { - Slog.wtf(TAG, - "Resetting option pendingIntentBackgroundActivityLaunchAllowedByPermission" - + " which is set by the pending intent creator (" - + packageName - + ") because this option is meant for the pending intent sender"); - if (CompatChanges.isChangeEnabled(PendingIntent.PENDING_INTENT_OPTIONS_CHECK, - callingUid)) { - throw new IllegalArgumentException( - "pendingIntentBackgroundActivityLaunchAllowedByPermission " - + "can not be set by creator of a PendingIntent"); - } - opts.setPendingIntentBackgroundActivityLaunchAllowedByPermission(false); - } - final boolean noCreate = (flags & PendingIntent.FLAG_NO_CREATE) != 0; final boolean cancelCurrent = (flags & PendingIntent.FLAG_CANCEL_CURRENT) != 0; final boolean updateCurrent = (flags & PendingIntent.FLAG_UPDATE_CURRENT) != 0; diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index 7f43fae72d60..31232687418f 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -174,6 +174,7 @@ public class SettingsToPropertiesMapper { "haptics", "hardware_backed_security_mainline", "input", + "incremental", "llvm_and_toolchains", "lse_desktop_experience", "machine_learning", diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index ca69f31adb35..8d8a54ea426d 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -31,6 +31,7 @@ import static android.media.AudioSystem.isBluetoothScoOutDevice; import static android.media.audio.Flags.automaticBtDeviceType; import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; +import static com.android.media.audio.Flags.asDeviceConnectionFailure; import android.annotation.NonNull; import android.annotation.Nullable; @@ -529,6 +530,17 @@ public class AudioDeviceInventory { } }; + /** + * package-protected for unit testing only + * Returns the currently connected devices + * @return the collection of connected devices + */ + /*package*/ @NonNull Collection<DeviceInfo> getConnectedDevices() { + synchronized (mDevicesLock) { + return mConnectedDevices.values(); + } + } + // List of devices actually connected to AudioPolicy (through AudioSystem), only one // by device type, which is used as the key, value is the DeviceInfo generated key. // For the moment only for A2DP sink devices. @@ -598,8 +610,9 @@ public class AudioDeviceInventory { /** * Class to store info about connected devices. * Use makeDeviceListKey() to make a unique key for this list. + * Package-protected for unit tests */ - private static class DeviceInfo { + /*package*/ static class DeviceInfo { final int mDeviceType; final @NonNull String mDeviceName; final @NonNull String mDeviceAddress; @@ -762,13 +775,27 @@ public class AudioDeviceInventory { // Always executed on AudioDeviceBroker message queue /*package*/ void onRestoreDevices() { synchronized (mDevicesLock) { + int res; + List<DeviceInfo> failedReconnectionDeviceList = new ArrayList<>(/*initialCapacity*/ 0); //TODO iterate on mApmConnectedDevices instead once it handles all device types for (DeviceInfo di : mConnectedDevices.values()) { - mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes(di.mDeviceType, + res = mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes( + di.mDeviceType, di.mDeviceAddress, di.mDeviceName), AudioSystem.DEVICE_STATE_AVAILABLE, di.mDeviceCodecFormat); + if (asDeviceConnectionFailure() && res != AudioSystem.AUDIO_STATUS_OK) { + failedReconnectionDeviceList.add(di); + } + } + if (asDeviceConnectionFailure()) { + for (DeviceInfo di : failedReconnectionDeviceList) { + AudioService.sDeviceLogger.enqueueAndSlog( + "Device inventory restore failed to reconnect " + di, + EventLogger.Event.ALOGE, TAG); + mConnectedDevices.remove(di.getKey(), di); + } } mAppliedStrategyRolesInt.clear(); mAppliedPresetRolesInt.clear(); @@ -2070,8 +2097,9 @@ public class AudioDeviceInventory { "APM failed to make available A2DP device addr=" + Utils.anonymizeBluetoothAddress(address) + " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG)); - // TODO: connection failed, stop here - // TODO: return; + if (asDeviceConnectionFailure()) { + return; + } } else { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "A2DP sink device addr=" + Utils.anonymizeBluetoothAddress(address) @@ -2336,8 +2364,7 @@ public class AudioDeviceInventory { "APM failed to make unavailable A2DP device addr=" + Utils.anonymizeBluetoothAddress(address) + " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG)); - // TODO: failed to disconnect, stop here - // TODO: return; + // not taking further action: proceeding as if disconnection from APM worked } else { AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent( "A2DP device addr=" + Utils.anonymizeBluetoothAddress(address) @@ -2383,8 +2410,9 @@ public class AudioDeviceInventory { "APM failed to make available A2DP source device addr=" + Utils.anonymizeBluetoothAddress(address) + " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG)); - // TODO: connection failed, stop here - // TODO: return + if (asDeviceConnectionFailure()) { + return; + } } else { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "A2DP source device addr=" + Utils.anonymizeBluetoothAddress(address) @@ -2402,6 +2430,7 @@ public class AudioDeviceInventory { mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_UNAVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT); + // always remove regardless of the result mConnectedDevices.remove( DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address)); mDeviceBroker.postCheckCommunicationDeviceRemoval(ada); @@ -2418,9 +2447,18 @@ public class AudioDeviceInventory { AudioDeviceAttributes ada = new AudioDeviceAttributes( DEVICE_OUT_HEARING_AID, address, name); - mAudioSystem.setDeviceConnectionState(ada, + final int res = mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT); + if (asDeviceConnectionFailure() && res != AudioSystem.AUDIO_STATUS_OK) { + AudioService.sDeviceLogger.enqueueAndSlog( + "APM failed to make available HearingAid addr=" + address + + " error=" + res, + EventLogger.Event.ALOGE, TAG); + return; + } + AudioService.sDeviceLogger.enqueueAndSlog("HearingAid made available addr=" + address, + EventLogger.Event.ALOGI, TAG); mConnectedDevices.put( DeviceInfo.makeDeviceListKey(DEVICE_OUT_HEARING_AID, address), new DeviceInfo(DEVICE_OUT_HEARING_AID, name, address)); @@ -2447,6 +2485,7 @@ public class AudioDeviceInventory { mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_UNAVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT); + // always remove regardless of return code mConnectedDevices.remove( DeviceInfo.makeDeviceListKey(DEVICE_OUT_HEARING_AID, address)); // Remove Hearing Aid routes as well @@ -2540,11 +2579,12 @@ public class AudioDeviceInventory { final int res = mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE, codec); if (res != AudioSystem.AUDIO_STATUS_OK) { - AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( + AudioService.sDeviceLogger.enqueueAndSlog( "APM failed to make available LE Audio device addr=" + address - + " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG)); - // TODO: connection failed, stop here - // TODO: return; + + " error=" + res, EventLogger.Event.ALOGE, TAG); + if (asDeviceConnectionFailure()) { + return; + } } else { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "LE Audio " + (AudioSystem.isInputDevice(device) ? "source" : "sink") @@ -2596,8 +2636,7 @@ public class AudioDeviceInventory { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "APM failed to make unavailable LE Audio device addr=" + address + " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG)); - // TODO: failed to disconnect, stop here - // TODO: return; + // not taking further action: proceeding as if disconnection from APM worked } else { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "LE Audio device addr=" + Utils.anonymizeBluetoothAddress(address) diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 1183768a272b..ac43e86a07c4 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -63,6 +63,7 @@ import static android.provider.Settings.Secure.VOLUME_HUSH_VIBRATE; import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; import static com.android.media.audio.Flags.absVolumeIndexFix; import static com.android.media.audio.Flags.alarmMinVolumeZero; +import static com.android.media.audio.Flags.asDeviceConnectionFailure; import static com.android.media.audio.Flags.audioserverPermissions; import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume; import static com.android.media.audio.Flags.replaceStreamBtSco; @@ -4306,7 +4307,8 @@ public class AudioService extends IAudioService.Stub super.getVolumeGroupVolumeIndex_enforcePermission(); synchronized (VolumeStreamState.class) { if (sVolumeGroupStates.indexOfKey(groupId) < 0) { - throw new IllegalArgumentException("No volume group for id " + groupId); + Log.e(TAG, "No volume group for id " + groupId); + return 0; } VolumeGroupState vgs = sVolumeGroupStates.get(groupId); // Return 0 when muted, not min index since for e.g. Voice Call, it has a non zero @@ -4322,7 +4324,8 @@ public class AudioService extends IAudioService.Stub super.getVolumeGroupMaxVolumeIndex_enforcePermission(); synchronized (VolumeStreamState.class) { if (sVolumeGroupStates.indexOfKey(groupId) < 0) { - throw new IllegalArgumentException("No volume group for id " + groupId); + Log.e(TAG, "No volume group for id " + groupId); + return 0; } VolumeGroupState vgs = sVolumeGroupStates.get(groupId); return vgs.getMaxIndex(); @@ -4336,7 +4339,8 @@ public class AudioService extends IAudioService.Stub super.getVolumeGroupMinVolumeIndex_enforcePermission(); synchronized (VolumeStreamState.class) { if (sVolumeGroupStates.indexOfKey(groupId) < 0) { - throw new IllegalArgumentException("No volume group for id " + groupId); + Log.e(TAG, "No volume group for id " + groupId); + return 0; } VolumeGroupState vgs = sVolumeGroupStates.get(groupId); return vgs.getMinIndex(); @@ -4765,6 +4769,8 @@ public class AudioService extends IAudioService.Stub private void dumpFlags(PrintWriter pw) { pw.println("\nFun with Flags:"); + pw.println("\tcom.android.media.audio.as_device_connection_failure:" + + asDeviceConnectionFailure()); pw.println("\tandroid.media.audio.autoPublicVolumeApiHardening:" + autoPublicVolumeApiHardening()); pw.println("\tandroid.media.audio.Flags.automaticBtDeviceType:" @@ -8259,11 +8265,21 @@ public class AudioService extends IAudioService.Stub private static final SparseArray<VolumeGroupState> sVolumeGroupStates = new SparseArray<>(); private void initVolumeGroupStates() { + int btScoGroupId = -1; + VolumeGroupState voiceCallGroup = null; for (final AudioVolumeGroup avg : getAudioVolumeGroups()) { try { - // if no valid attributes, this volume group is not controllable - if (ensureValidAttributes(avg)) { - sVolumeGroupStates.append(avg.getId(), new VolumeGroupState(avg)); + if (ensureValidVolumeGroup(avg)) { + final VolumeGroupState vgs = new VolumeGroupState(avg); + sVolumeGroupStates.append(avg.getId(), vgs); + if (vgs.isVoiceCall()) { + voiceCallGroup = vgs; + } + } else { + // invalid volume group will be reported for bt sco group with no other + // legacy stream type, we try to replace it in sVolumeGroupStates with the + // voice call volume group + btScoGroupId = avg.getId(); } } catch (IllegalArgumentException e) { // Volume Groups without attributes are not controllable through set/get volume @@ -8271,10 +8287,15 @@ public class AudioService extends IAudioService.Stub if (DEBUG_VOL) { Log.d(TAG, "volume group " + avg.name() + " for internal policy needs"); } - continue; } } + if (replaceStreamBtSco() && btScoGroupId >= 0 && voiceCallGroup != null) { + // the bt sco group is deprecated, storing the voice call group instead + // to keep the code backwards compatible when calling the volume group APIs + sVolumeGroupStates.append(btScoGroupId, voiceCallGroup); + } + // need mSettingsLock for vgs.applyAllVolumes -> vss.setIndex which grabs this lock after // VSS.class. Locking order needs to be preserved synchronized (mSettingsLock) { @@ -8285,7 +8306,15 @@ public class AudioService extends IAudioService.Stub } } - private boolean ensureValidAttributes(AudioVolumeGroup avg) { + /** + * Returns false if the legacy stream types only contains the deprecated + * {@link AudioSystem#STREAM_BLUETOOTH_SCO}. + * + * @throws IllegalArgumentException if it has more than one non-default {@link AudioAttributes} + * + * @param avg the volume group to check + */ + private boolean ensureValidVolumeGroup(AudioVolumeGroup avg) { boolean hasAtLeastOneValidAudioAttributes = avg.getAudioAttributes().stream() .anyMatch(aa -> !aa.equals(AudioProductStrategy.getDefaultAttributes())); if (!hasAtLeastOneValidAudioAttributes) { @@ -8293,10 +8322,11 @@ public class AudioService extends IAudioService.Stub + " has no valid audio attributes"); } if (replaceStreamBtSco()) { - for (int streamType : avg.getLegacyStreamTypes()) { - if (streamType == AudioSystem.STREAM_BLUETOOTH_SCO) { - return false; - } + // if there are multiple legacy stream types associated we can omit stream bt sco + // otherwise this is not a valid volume group + if (avg.getLegacyStreamTypes().length == 1 + && avg.getLegacyStreamTypes()[0] == AudioSystem.STREAM_BLUETOOTH_SCO) { + return false; } } return true; @@ -8637,6 +8667,10 @@ public class AudioService extends IAudioService.Stub return mHasValidStreamType && mPublicStreamType == AudioSystem.STREAM_MUSIC; } + public boolean isVoiceCall() { + return mHasValidStreamType && mPublicStreamType == AudioSystem.STREAM_VOICE_CALL; + } + public void applyAllVolumes(boolean userSwitch) { String caller = "from vgs"; synchronized (AudioService.VolumeStreamState.class) { diff --git a/services/core/java/com/android/server/biometrics/biometrics.aconfig b/services/core/java/com/android/server/biometrics/biometrics.aconfig index 92fd9cbcf14e..15c88500210e 100644 --- a/services/core/java/com/android/server/biometrics/biometrics.aconfig +++ b/services/core/java/com/android/server/biometrics/biometrics.aconfig @@ -14,3 +14,10 @@ flag { description: "This flag controls whether virtual HAL is used for testing instead of TestHal " bug: "294254230" } + +flag { + name: "notify_fingerprint_loe" + namespace: "biometrics_framework" + description: "This flag controls whether a notification should be sent to notify user when loss of enrollment happens" + bug: "351036558" +} diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java index 53e6bdb2ab5f..27f9cc88e28f 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java @@ -151,6 +151,43 @@ public class BiometricNotificationUtils { } /** + * Shows a fingerprint notification for loss of enrollment + */ + public static void showFingerprintLoeNotification(@NonNull Context context) { + Slog.d(TAG, "Showing fingerprint LOE notification"); + + final String name = + context.getString(R.string.device_unlock_notification_name); + final String title = context.getString(R.string.fingerprint_dangling_notification_title); + final String content = context.getString(R.string.fingerprint_loe_notification_msg); + + // Create "Set up" notification action button. + final Intent setupIntent = + new Intent(BiometricDanglingReceiver.ACTION_FINGERPRINT_RE_ENROLL_LAUNCH); + final PendingIntent setupPendingIntent = PendingIntent.getBroadcastAsUser(context, 0, + setupIntent, PendingIntent.FLAG_IMMUTABLE, UserHandle.CURRENT); + final String setupText = + context.getString(R.string.biometric_dangling_notification_action_set_up); + final Notification.Action setupAction = new Notification.Action.Builder( + null, setupText, setupPendingIntent).build(); + + // Create "Not now" notification action button. + final Intent notNowIntent = + new Intent(BiometricDanglingReceiver.ACTION_FINGERPRINT_RE_ENROLL_DISMISS); + final PendingIntent notNowPendingIntent = PendingIntent.getBroadcastAsUser(context, 0, + notNowIntent, PendingIntent.FLAG_IMMUTABLE, UserHandle.CURRENT); + final String notNowText = context.getString( + R.string.biometric_dangling_notification_action_not_now); + final Notification.Action notNowAction = new Notification.Action.Builder( + null, notNowText, notNowPendingIntent).build(); + + showNotificationHelper(context, name, title, content, setupPendingIntent, setupAction, + notNowAction, Notification.CATEGORY_SYSTEM, FINGERPRINT_RE_ENROLL_CHANNEL, + FINGERPRINT_RE_ENROLL_NOTIFICATION_TAG, Notification.VISIBILITY_SECRET, false, + Notification.FLAG_NO_CLEAR); + } + + /** * Shows a fingerprint bad calibration notification. */ public static void showBadCalibrationNotification(@NonNull Context context) { diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricUserState.java b/services/core/java/com/android/server/biometrics/sensors/BiometricUserState.java index 7fb27b6896da..63678aaa16c3 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricUserState.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricUserState.java @@ -57,6 +57,7 @@ public abstract class BiometricUserState<T extends BiometricAuthenticator.Identi protected boolean mInvalidationInProgress; protected final Context mContext; protected final File mFile; + private boolean mIsInvalidBiometricState = false; private final Runnable mWriteStateRunnable = this::doWriteStateInternal; @@ -102,7 +103,7 @@ public abstract class BiometricUserState<T extends BiometricAuthenticator.Identi serializer.endDocument(); destination.finishWrite(out); } catch (Throwable t) { - Slog.wtf(TAG, "Failed to write settings, restoring backup", t); + Slog.e(TAG, "Failed to write settings, restoring backup", t); destination.failWrite(out); throw new IllegalStateException("Failed to write to file: " + mFile.toString(), t); } finally { @@ -192,6 +193,29 @@ public abstract class BiometricUserState<T extends BiometricAuthenticator.Identi } } + /** + * Return true if the biometric file is correctly read. Otherwise return false. + */ + public boolean isInvalidBiometricState() { + return mIsInvalidBiometricState; + } + + /** + * Delete the file of the biometric state. + */ + public void deleteBiometricFile() { + synchronized (this) { + if (!mFile.exists()) { + return; + } + if (mFile.delete()) { + Slog.i(TAG, mFile + " is deleted successfully"); + } else { + Slog.i(TAG, "Failed to delete " + mFile); + } + } + } + private boolean isUnique(String name) { for (T identifier : mBiometrics) { if (identifier.getName().equals(name)) { @@ -218,7 +242,8 @@ public abstract class BiometricUserState<T extends BiometricAuthenticator.Identi try { in = new FileInputStream(mFile); } catch (FileNotFoundException fnfe) { - Slog.i(TAG, "No fingerprint state"); + Slog.i(TAG, "No fingerprint state", fnfe); + mIsInvalidBiometricState = true; return; } try { diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricUtils.java b/services/core/java/com/android/server/biometrics/sensors/BiometricUtils.java index ebe467942790..0b4f64042055 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricUtils.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricUtils.java @@ -33,4 +33,14 @@ public interface BiometricUtils<T extends BiometricAuthenticator.Identifier> { CharSequence getUniqueName(Context context, int userId); void setInvalidationInProgress(Context context, int userId, boolean inProgress); boolean isInvalidationInProgress(Context context, int userId); + + /** + * Return true if the biometric file is correctly read. Otherwise return false. + */ + boolean hasValidBiometricUserState(Context context, int userId); + + /** + * Delete the file of the biometric state. + */ + void deleteStateForUser(int userId); }
\ No newline at end of file diff --git a/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java b/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java index 69ad1523118d..3b6aeef92421 100644 --- a/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java @@ -25,6 +25,7 @@ import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import com.android.server.biometrics.BiometricsProto; +import com.android.server.biometrics.Flags; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; @@ -62,7 +63,7 @@ public abstract class InternalCleanupClient<S extends BiometricAuthenticator.Ide } private final ArrayList<UserTemplate> mUnknownHALTemplates = new ArrayList<>(); - private final BiometricUtils<S> mBiometricUtils; + protected final BiometricUtils<S> mBiometricUtils; private final Map<Integer, Long> mAuthenticatorIds; private final boolean mHasEnrollmentsBeforeStarting; private BaseClientMonitor mCurrentTask; @@ -105,6 +106,11 @@ public abstract class InternalCleanupClient<S extends BiometricAuthenticator.Ide startCleanupUnknownHalTemplates(); } } + + if (mBiometricUtils.hasValidBiometricUserState(getContext(), getTargetUserId()) + && Flags.notifyFingerprintLoe()) { + handleInvalidBiometricState(); + } } }; @@ -248,4 +254,8 @@ public abstract class InternalCleanupClient<S extends BiometricAuthenticator.Ide public ArrayList<UserTemplate> getUnknownHALTemplates() { return mUnknownHALTemplates; } + + protected void handleInvalidBiometricState() {} + + protected abstract int getModality(); } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceUtils.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceUtils.java index c5744780cd71..79285cbd9ea5 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/FaceUtils.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceUtils.java @@ -124,6 +124,22 @@ public class FaceUtils implements BiometricUtils<Face> { return getStateForUser(context, userId).isInvalidationInProgress(); } + @Override + public boolean hasValidBiometricUserState(Context context, int userId) { + return getStateForUser(context, userId).isInvalidBiometricState(); + } + + @Override + public void deleteStateForUser(int userId) { + synchronized (this) { + FaceUserState state = mUserStates.get(userId); + if (state != null) { + state.deleteBiometricFile(); + mUserStates.delete(userId); + } + } + } + private FaceUserState getStateForUser(Context ctx, int userId) { synchronized (this) { FaceUserState state = mUserStates.get(userId); diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalCleanupClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalCleanupClient.java index e75c6aba1489..964bf6cad63c 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalCleanupClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalCleanupClient.java @@ -19,6 +19,7 @@ package com.android.server.biometrics.sensors.face.aidl; import android.annotation.NonNull; import android.content.Context; import android.hardware.biometrics.BiometricAuthenticator; +import android.hardware.biometrics.BiometricsProtoEnums; import android.hardware.biometrics.face.IFace; import android.hardware.face.Face; import android.os.IBinder; @@ -77,4 +78,9 @@ public class FaceInternalCleanupClient extends InternalCleanupClient<Face, AidlS FaceUtils.getInstance(getSensorId()).addBiometricForUser( getContext(), getTargetUserId(), (Face) identifier); } + + @Override + protected int getModality() { + return BiometricsProtoEnums.MODALITY_FACE; + } } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintUtils.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintUtils.java index 0062d31962a9..b8c06c730edc 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintUtils.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintUtils.java @@ -140,6 +140,22 @@ public class FingerprintUtils implements BiometricUtils<Fingerprint> { return getStateForUser(context, userId).isInvalidationInProgress(); } + @Override + public boolean hasValidBiometricUserState(Context context, int userId) { + return getStateForUser(context, userId).isInvalidBiometricState(); + } + + @Override + public void deleteStateForUser(int userId) { + synchronized (this) { + FingerprintUserState state = mUserStates.get(userId); + if (state != null) { + state.deleteBiometricFile(); + mUserStates.delete(userId); + } + } + } + private FingerprintUserState getStateForUser(Context ctx, int userId) { synchronized (this) { FingerprintUserState state = mUserStates.get(userId); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClient.java index 5edc2ca080ad..1fc517906c58 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClient.java @@ -22,9 +22,11 @@ import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricsProtoEnums; import android.hardware.fingerprint.Fingerprint; import android.os.IBinder; +import android.util.Slog; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; +import com.android.server.biometrics.sensors.BiometricNotificationUtils; import com.android.server.biometrics.sensors.BiometricUtils; import com.android.server.biometrics.sensors.InternalCleanupClient; import com.android.server.biometrics.sensors.InternalEnumerateClient; @@ -42,6 +44,8 @@ import java.util.function.Supplier; public class FingerprintInternalCleanupClient extends InternalCleanupClient<Fingerprint, AidlSession> { + private static final String TAG = "FingerprintInternalCleanupClient"; + public FingerprintInternalCleanupClient(@NonNull Context context, @NonNull Supplier<AidlSession> lazyDaemon, int userId, @NonNull String owner, int sensorId, @@ -80,4 +84,16 @@ public class FingerprintInternalCleanupClient FingerprintUtils.getInstance(getSensorId()).addBiometricForUser( getContext(), getTargetUserId(), (Fingerprint) identifier); } + + @Override + public void handleInvalidBiometricState() { + Slog.d(TAG, "Invalid fingerprint user state: delete the state."); + mBiometricUtils.deleteStateForUser(getTargetUserId()); + BiometricNotificationUtils.showFingerprintLoeNotification(getContext()); + } + + @Override + protected int getModality() { + return BiometricsProtoEnums.MODALITY_FINGERPRINT; + } } diff --git a/services/core/java/com/android/server/crashrecovery/TEST_MAPPING b/services/core/java/com/android/server/crashrecovery/TEST_MAPPING index 537fb32523b5..615db345635c 100644 --- a/services/core/java/com/android/server/crashrecovery/TEST_MAPPING +++ b/services/core/java/com/android/server/crashrecovery/TEST_MAPPING @@ -1,9 +1,4 @@ { - "presubmit": [ - { - "name": "CrashRecoveryModuleTests" - } - ], "postsubmit": [ { "name": "FrameworksMockingServicesTests", @@ -12,6 +7,9 @@ "include-filter": "com.android.server.RescuePartyTest" } ] + }, + { + "name": "CrashRecoveryModuleTests" } ] }
\ No newline at end of file diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java index 7b5cff739ba1..226bdf54ce3b 100644 --- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java +++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java @@ -579,6 +579,14 @@ public class AutomaticBrightnessController { return mCurrentBrightnessMapper.getMode(); } + /** + * @return The preset for this mapping strategy. Presets are used on devices that allow users + * to choose from a set of predefined options in display auto-brightness settings. + */ + public int getPreset() { + return mCurrentBrightnessMapper.getPreset(); + } + public boolean isInIdleMode() { return mCurrentBrightnessMapper.getMode() == AUTO_BRIGHTNESS_MODE_IDLE; } diff --git a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java index 8405e0a52084..b0507fb78a41 100644 --- a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java +++ b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java @@ -140,10 +140,10 @@ public abstract class BrightnessMappingStrategy { builder.setShortTermModelLowerLuxMultiplier(SHORT_TERM_MODEL_THRESHOLD_RATIO); builder.setShortTermModelUpperLuxMultiplier(SHORT_TERM_MODEL_THRESHOLD_RATIO); return new PhysicalMappingStrategy(builder.build(), nitsRange, brightnessRange, - autoBrightnessAdjustmentMaxGamma, mode, displayWhiteBalanceController); + autoBrightnessAdjustmentMaxGamma, mode, preset, displayWhiteBalanceController); } else if (isValidMapping(luxLevels, brightnessLevels)) { return new SimpleMappingStrategy(luxLevels, brightnessLevels, - autoBrightnessAdjustmentMaxGamma, shortTermModelTimeout, mode); + autoBrightnessAdjustmentMaxGamma, shortTermModelTimeout, mode, preset); } else { return null; } @@ -394,6 +394,12 @@ public abstract class BrightnessMappingStrategy { abstract int getMode(); /** + * @return The preset for this mapping strategy. Presets are used on devices that allow users + * to choose from a set of predefined options in display auto-brightness settings. + */ + abstract int getPreset(); + + /** * Check if the short term model should be reset given the anchor lux the last * brightness change was made at and the current ambient lux. */ @@ -598,6 +604,8 @@ public abstract class BrightnessMappingStrategy { @AutomaticBrightnessController.AutomaticBrightnessMode private final int mMode; + private final int mPreset; + private Spline mSpline; private float mMaxGamma; private float mAutoBrightnessAdjustment; @@ -606,7 +614,8 @@ public abstract class BrightnessMappingStrategy { private long mShortTermModelTimeout; private SimpleMappingStrategy(float[] lux, float[] brightness, float maxGamma, - long timeout, @AutomaticBrightnessController.AutomaticBrightnessMode int mode) { + long timeout, @AutomaticBrightnessController.AutomaticBrightnessMode int mode, + int preset) { Preconditions.checkArgument(lux.length != 0 && brightness.length != 0, "Lux and brightness arrays must not be empty!"); Preconditions.checkArgument(lux.length == brightness.length, @@ -633,6 +642,7 @@ public abstract class BrightnessMappingStrategy { computeSpline(); mShortTermModelTimeout = timeout; mMode = mode; + mPreset = preset; } @Override @@ -766,6 +776,11 @@ public abstract class BrightnessMappingStrategy { } @Override + int getPreset() { + return mPreset; + } + + @Override float getUserLux() { return mUserLux; } @@ -837,6 +852,8 @@ public abstract class BrightnessMappingStrategy { @AutomaticBrightnessController.AutomaticBrightnessMode private final int mMode; + private final int mPreset; + // Previous short-term models and the times that they were computed stored for debugging // purposes private List<Spline> mPreviousBrightnessSplines = new ArrayList<>(); @@ -846,7 +863,7 @@ public abstract class BrightnessMappingStrategy { public PhysicalMappingStrategy(BrightnessConfiguration config, float[] nits, float[] brightness, float maxGamma, - @AutomaticBrightnessController.AutomaticBrightnessMode int mode, + @AutomaticBrightnessController.AutomaticBrightnessMode int mode, int preset, @Nullable DisplayWhiteBalanceController displayWhiteBalanceController) { Preconditions.checkArgument(nits.length != 0 && brightness.length != 0, @@ -860,6 +877,7 @@ public abstract class BrightnessMappingStrategy { PowerManager.BRIGHTNESS_MIN, PowerManager.BRIGHTNESS_MAX, "brightness"); mMode = mode; + mPreset = preset; mMaxGamma = maxGamma; mAutoBrightnessAdjustment = 0; mUserLux = INVALID_LUX; @@ -1073,6 +1091,11 @@ public abstract class BrightnessMappingStrategy { } @Override + int getPreset() { + return mPreset; + } + + @Override float getUserLux() { return mUserLux; } diff --git a/services/core/java/com/android/server/display/BrightnessRangeController.java b/services/core/java/com/android/server/display/BrightnessRangeController.java index 515e70495f9e..8a3e39257145 100644 --- a/services/core/java/com/android/server/display/BrightnessRangeController.java +++ b/services/core/java/com/android/server/display/BrightnessRangeController.java @@ -60,7 +60,7 @@ class BrightnessRangeController { mModeChangeCallback = modeChangeCallback; mHdrClamper = hdrClamper; mNormalBrightnessModeController = normalBrightnessModeController; - mUseHdrClamper = flags.isHdrClamperEnabled(); + mUseHdrClamper = flags.isHdrClamperEnabled() && !flags.useNewHdrBrightnessModifier(); mUseNbmController = flags.isNbmControllerEnabled(); if (mUseNbmController) { mNormalBrightnessModeController.resetNbmData( diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index ed6ed60a6806..cc115f13f5e3 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -588,22 +588,43 @@ import javax.xml.datatype.DatatypeConfigurationException; * <minorVersion>0</minorVersion> * </usiVersion> * <evenDimmer enabled="true"> - * <transitionPoint>0.1</transitionPoint> - * - * <nits>0.2</nits> - * <nits>2.0</nits> - * <nits>500.0</nits> - * <nits>1000.0</nits> - * - * <backlight>0</backlight> - * <backlight>0.0001</backlight> - * <backlight>0.5</backlight> - * <backlight>1.0</backlight> - * - * <brightness>0</brightness> - * <brightness>0.1</brightness> - * <brightness>0.5</brightness> - * <brightness>1.0</brightness> + * <transitionPoint>0.1</transitionPoint> + * <brightnessMapping> + * <brightnessPoint> + * <nits>0.2</nits> + * <backlight>0</backlight> + * <brightness>0</brightness> + * </brightnessPoint> + * <brightnessPoint> + * <nits>2.0</nits> + * <backlight>0.01</backlight> + * <brightness>0.002</brightness> + * </brightnessPoint> + * <brightnessPoint> + * <nits>500.0</nits> + * <backlight>0.5</backlight> + * <brightness>0.5</brightness> + * </brightnessPoint> + * <brightnessPoint> + * <nits>1000</nits> + * <backlight>1.0</backlight> + * <brightness>1.0</brightness> + * </brightnessPoint> + * </brightnessMapping> + * <luxToMinimumNitsMap> + * <point> + * <value>10</value> + * <nits>0.3</nits> + * </point> + * <point> + * <value>50</value> + * <nits>0.7</nits> + * </point> + * <point> + * <value>100</value> + * <nits>1.0</nits> + * </point> + * </luxToMinimumNitsMap> * </evenDimmer> * <screenBrightnessCapForWearBedtimeMode>0.1</screenBrightnessCapForWearBedtimeMode> * <idleScreenRefreshRateTimeout> diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 2cec869c290e..9e905abd78ed 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -722,6 +722,7 @@ public final class DisplayManagerService extends SystemService { if (userSwitching) { mCurrentUserId = newUserId; } + mDisplayModeDirector.onSwitchUser(); mLogicalDisplayMapper.forEachLocked(logicalDisplay -> { if (logicalDisplay.getDisplayInfoLocked().type != Display.TYPE_INTERNAL) { return; diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 8b21d98045dd..480c370aff86 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -702,6 +702,17 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call private void handleOnSwitchUser(@UserIdInt int newUserId, int userSerial, float newBrightness) { Slog.i(mTag, "Switching user newUserId=" + newUserId + " userSerial=" + userSerial + " newBrightness=" + newBrightness); + + if (mAutomaticBrightnessController != null) { + int autoBrightnessPreset = Settings.System.getIntForUser(mContext.getContentResolver(), + Settings.System.SCREEN_BRIGHTNESS_FOR_ALS, + Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL, + UserHandle.USER_CURRENT); + if (autoBrightnessPreset != mAutomaticBrightnessController.getPreset()) { + setUpAutoBrightness(mContext, mHandler); + } + } + handleBrightnessModeChange(); if (mBrightnessTracker != null) { mBrightnessTracker.onSwitchUser(newUserId); @@ -714,6 +725,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call if (mAutomaticBrightnessController != null) { mAutomaticBrightnessController.resetShortTermModel(); } + mBrightnessClamperController.onUserSwitch(); sendUpdatePowerState(); } @@ -1009,7 +1021,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call if (mFlags.areAutoBrightnessModesEnabled()) { mContext.getContentResolver().registerContentObserver( Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS_FOR_ALS), - /* notifyForDescendants= */ false, mSettingsObserver, UserHandle.USER_CURRENT); + /* notifyForDescendants= */ false, mSettingsObserver, UserHandle.USER_ALL); } handleBrightnessModeChange(); } diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java index 9324fc1c4e06..12c3197aba2a 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java @@ -71,6 +71,7 @@ public class BrightnessClamperController { private final List<DisplayDeviceDataListener> mDisplayDeviceDataListeners = new ArrayList<>(); private final List<StatefulModifier> mStatefulModifiers = new ArrayList<>(); + private final List<UserSwitchListener> mUserSwitchListeners = new ArrayList<>(); private ModifiersAggregatedState mModifiersAggregatedState = new ModifiersAggregatedState(); private final DeviceConfig.OnPropertiesChangedListener mOnPropertiesChangedListener; @@ -127,6 +128,9 @@ public class BrightnessClamperController { if (m instanceof StatefulModifier s) { mStatefulModifiers.add(s); } + if (m instanceof UserSwitchListener l) { + mUserSwitchListeners.add(l); + } }); mOnPropertiesChangedListener = properties -> mClampers.forEach(BrightnessClamper::onDeviceConfigChanged); @@ -209,6 +213,13 @@ public class BrightnessClamperController { } /** + * Called when the user switches. + */ + public void onUserSwitch() { + mUserSwitchListeners.forEach(listener -> listener.onSwitchUser()); + } + + /** * Used to dump ClampersController state. */ public void dump(PrintWriter writer) { @@ -466,6 +477,13 @@ public class BrightnessClamperController { } /** + * A clamper/modifier should implement this interface if it reads user-specific settings + */ + interface UserSwitchListener { + void onSwitchUser(); + } + + /** * StatefulModifiers contribute to AggregatedState, that is used to decide if brightness * adjustement is needed */ diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java index 951980adac8c..c3596c3e77fe 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java @@ -41,7 +41,8 @@ import java.io.PrintWriter; * Class used to prevent the screen brightness dipping below a certain value, based on current * lux conditions and user preferred minimum. */ -public class BrightnessLowLuxModifier extends BrightnessModifier { +public class BrightnessLowLuxModifier extends BrightnessModifier implements + BrightnessClamperController.UserSwitchListener { // To enable these logs, run: // 'adb shell setprop persist.log.tag.BrightnessLowLuxModifier DEBUG && adb reboot' @@ -81,10 +82,9 @@ public class BrightnessLowLuxModifier extends BrightnessModifier { */ @VisibleForTesting public void recalculateLowerBound() { - int userId = UserHandle.USER_CURRENT; float settingNitsLowerBound = Settings.Secure.getFloatForUser( mContentResolver, Settings.Secure.EVEN_DIMMER_MIN_NITS, - /* def= */ MIN_NITS_DEFAULT, userId); + /* def= */ MIN_NITS_DEFAULT, UserHandle.USER_CURRENT); boolean isActive = isSettingEnabled() && mAmbientLux != BrightnessMappingStrategy.INVALID_LUX; @@ -190,6 +190,11 @@ public class BrightnessLowLuxModifier extends BrightnessModifier { } @Override + public void onSwitchUser() { + recalculateLowerBound(); + } + + @Override public void dump(PrintWriter pw) { pw.println("BrightnessLowLuxModifier:"); pw.println(" mIsActive=" + mIsActive); @@ -221,10 +226,10 @@ public class BrightnessLowLuxModifier extends BrightnessModifier { super(handler); mContentResolver.registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.EVEN_DIMMER_MIN_NITS), - false, this); + false, this, UserHandle.USER_ALL); mContentResolver.registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.EVEN_DIMMER_ACTIVATED), - false, this); + false, this, UserHandle.USER_ALL); } @Override diff --git a/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java b/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java index 5e44cc357b28..ae1801ccea74 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java +++ b/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java @@ -31,6 +31,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.display.BrightnessSynchronizer; import com.android.server.display.DisplayBrightnessState; import com.android.server.display.DisplayDeviceConfig; +import com.android.server.display.brightness.BrightnessReason; import com.android.server.display.config.HdrBrightnessData; import java.io.PrintWriter; @@ -99,7 +100,7 @@ public class HdrBrightnessModifier implements BrightnessStateModifier, mMaxBrightness = mPendingMaxBrightness; mClamperChangeListener.onChanged(); }; - onDisplayChanged(displayData); + mHandler.post(() -> onDisplayChanged(displayData)); } // Called in DisplayControllerHandler @@ -120,6 +121,8 @@ public class HdrBrightnessModifier implements BrightnessStateModifier, stateBuilder.setHdrBrightness(hdrBrightness); stateBuilder.setCustomAnimationRate(mTransitionRate); + stateBuilder.getBrightnessReason().addModifier(BrightnessReason.MODIFIER_HDR); + // transition rate applied, reset mTransitionRate = CUSTOM_ANIMATION_RATE_NOT_SET; } @@ -168,10 +171,18 @@ public class HdrBrightnessModifier implements BrightnessStateModifier, } } + // Called in DisplayControllerHandler @Override public void onDisplayChanged(BrightnessClamperController.DisplayDeviceData displayData) { - mHandler.post(() -> onDisplayChanged(displayData.mDisplayToken, displayData.mWidth, - displayData.mHeight, displayData.mDisplayDeviceConfig)); + mDisplayDeviceConfig = displayData.mDisplayDeviceConfig; + mScreenSize = (float) displayData.mWidth * displayData.mHeight; + HdrBrightnessData data = mDisplayDeviceConfig.getHdrBrightnessData(); + if (data == null) { + unregisterHdrListener(); + } else { + registerHdrListener(displayData.mDisplayToken); + } + recalculate(data, mMaxDesiredHdrRatio); } // Called in DisplayControllerHandler, when any modifier state changes @@ -215,20 +226,6 @@ public class HdrBrightnessModifier implements BrightnessStateModifier, } // Called in DisplayControllerHandler - private void onDisplayChanged(IBinder displayToken, int width, int height, - DisplayDeviceConfig config) { - mDisplayDeviceConfig = config; - mScreenSize = (float) width * height; - HdrBrightnessData data = config.getHdrBrightnessData(); - if (data == null) { - unregisterHdrListener(); - } else { - registerHdrListener(displayToken); - } - recalculate(data, mMaxDesiredHdrRatio); - } - - // Called in DisplayControllerHandler private void recalculate(@Nullable HdrBrightnessData data, float maxDesiredHdrRatio) { Mode newMode = recalculateMode(data); // if HDR mode changed, notify changed @@ -258,6 +255,10 @@ public class HdrBrightnessModifier implements BrightnessStateModifier, if (data == null) { return Mode.NO_HDR; } + // no HDR layer present + if (mHdrLayerSize == DEFAULT_HDR_LAYER_SIZE) { + return Mode.NO_HDR; + } // HDR layer < minHdr % for Nbm if (mHdrLayerSize < mScreenSize * data.minimumHdrPercentOfScreenForNbm) { return Mode.NO_HDR; diff --git a/services/core/java/com/android/server/display/brightness/clamper/LightSensorController.java b/services/core/java/com/android/server/display/brightness/clamper/LightSensorController.java index d89dd28c4a89..b219cb1bc15c 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/LightSensorController.java +++ b/services/core/java/com/android/server/display/brightness/clamper/LightSensorController.java @@ -62,6 +62,9 @@ public class LightSensorController { private final SensorEventListener mLightSensorEventListener = new SensorEventListener() { @Override public void onSensorChanged(SensorEvent event) { + if (event.sensor != mRegisteredLightSensor) { + return; + } long now = mInjector.getTime(); mAmbientFilter.addValue(TimeUnit.NANOSECONDS.toMillis(event.timestamp), event.values[0]); @@ -95,15 +98,13 @@ public class LightSensorController { if (mRegisteredLightSensor == mLightSensor) { return; } + if (mLightSensor != null) { + mSensorManager.registerListener(mLightSensorEventListener, + mLightSensor, mLightSensorRate * 1000, mHandler); + } if (mRegisteredLightSensor != null) { stop(); } - if (mLightSensor == null) { - return; - } - - mSensorManager.registerListener(mLightSensorEventListener, - mLightSensor, mLightSensorRate * 1000, mHandler); mRegisteredLightSensor = mLightSensor; if (DEBUG) { @@ -115,7 +116,7 @@ public class LightSensorController { if (mRegisteredLightSensor == null) { return; } - mSensorManager.unregisterListener(mLightSensorEventListener); + mSensorManager.unregisterListener(mLightSensorEventListener, mRegisteredLightSensor); mRegisteredLightSensor = null; mAmbientFilter.clear(); mLightSensorListener.onAmbientLuxChange(INVALID_LUX); diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java index d610f086b3b5..5e471c82e108 100644 --- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java +++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java @@ -121,6 +121,7 @@ public class DisplayModeDirector { private static final int MSG_HIGH_BRIGHTNESS_THRESHOLDS_CHANGED = 6; private static final int MSG_REFRESH_RATE_IN_HBM_SUNLIGHT_CHANGED = 7; private static final int MSG_REFRESH_RATE_IN_HBM_HDR_CHANGED = 8; + private static final int MSG_SWITCH_USER = 9; private final Object mLock = new Object(); private final Context mContext; @@ -564,6 +565,13 @@ public class DisplayModeDirector { } /** + * Called when the user switches. + */ + public void onSwitchUser() { + mHandler.obtainMessage(MSG_SWITCH_USER).sendToTarget(); + } + + /** * Print the object's state and debug information into the given stream. * * @param pw The stream to dump information to. @@ -789,6 +797,13 @@ public class DisplayModeDirector { mHbmObserver.onDeviceConfigRefreshRateInHbmHdrChanged(refreshRateInHbmHdr); break; } + + case MSG_SWITCH_USER: { + synchronized (mLock) { + mSettingsObserver.updateRefreshRateSettingLocked(); + mSettingsObserver.updateModeSwitchingTypeSettingLocked(); + } + } } } } @@ -1012,10 +1027,10 @@ public class DisplayModeDirector { final ContentResolver cr = mContext.getContentResolver(); mInjector.registerPeakRefreshRateObserver(cr, this); mInjector.registerMinRefreshRateObserver(cr, this); - cr.registerContentObserver(mLowPowerModeSetting, false /*notifyDescendants*/, this, - UserHandle.USER_SYSTEM); - cr.registerContentObserver(mMatchContentFrameRateSetting, false /*notifyDescendants*/, - this); + cr.registerContentObserver(mLowPowerModeSetting, /* notifyDescendants= */ false, this, + UserHandle.USER_ALL); + cr.registerContentObserver(mMatchContentFrameRateSetting, + /* notifyDescendants= */ false, this, UserHandle.USER_ALL); mInjector.registerDisplayListener(mDisplayListener, mHandler); float deviceConfigDefaultPeakRefresh = @@ -1156,14 +1171,15 @@ public class DisplayModeDirector { float highestRefreshRate = getMaxRefreshRateLocked(displayId); float minRefreshRate = Settings.System.getFloatForUser(cr, - Settings.System.MIN_REFRESH_RATE, 0f, cr.getUserId()); + Settings.System.MIN_REFRESH_RATE, 0f, UserHandle.USER_CURRENT); if (Float.isInfinite(minRefreshRate)) { // Infinity means that we want the highest possible refresh rate minRefreshRate = highestRefreshRate; } float peakRefreshRate = Settings.System.getFloatForUser(cr, - Settings.System.PEAK_REFRESH_RATE, mDefaultPeakRefreshRate, cr.getUserId()); + Settings.System.PEAK_REFRESH_RATE, mDefaultPeakRefreshRate, + UserHandle.USER_CURRENT); if (Float.isInfinite(peakRefreshRate)) { // Infinity means that we want the highest possible refresh rate peakRefreshRate = highestRefreshRate; @@ -1234,9 +1250,9 @@ public class DisplayModeDirector { private void updateModeSwitchingTypeSettingLocked() { final ContentResolver cr = mContext.getContentResolver(); - int switchingType = Settings.Secure.getIntForUser( - cr, Settings.Secure.MATCH_CONTENT_FRAME_RATE, mModeSwitchingType /*default*/, - cr.getUserId()); + int switchingType = Settings.Secure.getIntForUser(cr, + Settings.Secure.MATCH_CONTENT_FRAME_RATE, /* default= */ mModeSwitchingType, + UserHandle.USER_CURRENT); if (switchingType != mModeSwitchingType) { mModeSwitchingType = switchingType; notifyDesiredDisplayModeSpecsChangedLocked(); @@ -3033,14 +3049,14 @@ public class DisplayModeDirector { public void registerPeakRefreshRateObserver(@NonNull ContentResolver cr, @NonNull ContentObserver observer) { cr.registerContentObserver(PEAK_REFRESH_RATE_URI, false /*notifyDescendants*/, - observer, UserHandle.USER_SYSTEM); + observer, UserHandle.USER_ALL); } @Override public void registerMinRefreshRateObserver(@NonNull ContentResolver cr, @NonNull ContentObserver observer) { cr.registerContentObserver(MIN_REFRESH_RATE_URI, false /*notifyDescendants*/, - observer, UserHandle.USER_SYSTEM); + observer, UserHandle.USER_ALL); } @Override diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java index d43e783cad41..a3b77e897117 100644 --- a/services/core/java/com/android/server/dreams/DreamManagerService.java +++ b/services/core/java/com/android/server/dreams/DreamManagerService.java @@ -543,18 +543,20 @@ public final class DreamManagerService extends SystemService { } private void startDozingInternal(IBinder token, int screenState, - @Display.StateReason int reason, int screenBrightness) { + @Display.StateReason int reason, float screenBrightnessFloat, int screenBrightnessInt) { Slog.d(TAG, "Dream requested to start dozing: " + token + ", screenState=" + Display.stateToString(screenState) + ", reason=" + Display.stateReasonToString(reason) - + ", screenBrightness=" + screenBrightness); + + ", screenBrightnessFloat=" + screenBrightnessFloat + + ", screenBrightnessInt=" + screenBrightnessInt); synchronized (mLock) { if (mCurrentDream != null && mCurrentDream.token == token && mCurrentDream.canDoze) { mCurrentDream.dozeScreenState = screenState; - mCurrentDream.dozeScreenBrightness = screenBrightness; + mCurrentDream.dozeScreenBrightness = screenBrightnessInt; + mCurrentDream.dozeScreenBrightnessFloat = screenBrightnessFloat; mPowerManagerInternal.setDozeOverrideFromDreamManager( - screenState, reason, screenBrightness); + screenState, reason, screenBrightnessFloat, screenBrightnessInt); if (!mCurrentDream.isDozing) { mCurrentDream.isDozing = true; mDozeWakeLock.acquire(); @@ -575,6 +577,7 @@ public final class DreamManagerService extends SystemService { mPowerManagerInternal.setDozeOverrideFromDreamManager( Display.STATE_UNKNOWN, Display.STATE_REASON_DREAM_MANAGER, + PowerManager.BRIGHTNESS_INVALID_FLOAT, PowerManager.BRIGHTNESS_DEFAULT); } } @@ -1095,7 +1098,7 @@ public final class DreamManagerService extends SystemService { @Override // Binder call public void startDozing( IBinder token, int screenState, @Display.StateReason int reason, - int screenBrightness) { + float screenBrightnessFloat, int screeBrightnessInt) { // Requires no permission, called by Dream from an arbitrary process. if (token == null) { throw new IllegalArgumentException("token must not be null"); @@ -1103,7 +1106,8 @@ public final class DreamManagerService extends SystemService { final long ident = Binder.clearCallingIdentity(); try { - startDozingInternal(token, screenState, reason, screenBrightness); + startDozingInternal(token, screenState, reason, screenBrightnessFloat, + screeBrightnessInt); } finally { Binder.restoreCallingIdentity(ident); } @@ -1112,7 +1116,7 @@ public final class DreamManagerService extends SystemService { @Override // Binder call public void startDozingOneway( IBinder token, int screenState, @Display.StateReason int reason, - int screenBrightness) { + float screenBrightnessFloat, int screeBrightnessInt) { // Requires no permission, called by Dream from an arbitrary process. if (token == null) { throw new IllegalArgumentException("token must not be null"); @@ -1120,7 +1124,8 @@ public final class DreamManagerService extends SystemService { final long ident = Binder.clearCallingIdentity(); try { - startDozingInternal(token, screenState, reason, screenBrightness); + startDozingInternal(token, screenState, reason, screenBrightnessFloat, + screeBrightnessInt); } finally { Binder.restoreCallingIdentity(ident); } @@ -1277,6 +1282,7 @@ public final class DreamManagerService extends SystemService { public boolean isWaking = false; public int dozeScreenState = Display.STATE_UNKNOWN; public int dozeScreenBrightness = PowerManager.BRIGHTNESS_DEFAULT; + public float dozeScreenBrightnessFloat = PowerManager.BRIGHTNESS_INVALID_FLOAT; DreamRecord(ComponentName name, int userId, boolean isPreview, boolean canDoze) { this.name = name; @@ -1297,6 +1303,7 @@ public final class DreamManagerService extends SystemService { + ", isWaking=" + isWaking + ", dozeScreenState=" + dozeScreenState + ", dozeScreenBrightness=" + dozeScreenBrightness + + ", dozeScreenBrightnessFloat=" + dozeScreenBrightnessFloat + '}'; } } diff --git a/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java b/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java index 3c3bdd5b69f6..7746276ac505 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java @@ -769,6 +769,7 @@ public class HdmiCecMessageValidator { * @return true if the UI Broadcast type is valid */ private static boolean isValidUiBroadcastType(int value) { + value = value & 0xFF; return ((value == 0x00) || (value == 0x01) || (value == 0x10) diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java b/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java index 7f7ae1071bfb..58e345207edd 100644 --- a/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java +++ b/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java @@ -71,7 +71,20 @@ final class IInputMethodManagerImpl extends IInputMethodManager.Stub { @Retention(SOURCE) @Target({METHOD}) @interface PermissionVerified { + /** + * The name of the permission that is verified, if precisely one permission is required. + * If more than one permission is required, specify either {@link #allOf()} instead. + * + * <p>If specified, {@link #allOf()} must both be {@code null}.</p> + */ String value() default ""; + + /** + * Specifies a list of permission names that are all required. + * + * <p>If specified, {@link #value()} must both be {@code null}.</p> + */ + String[] allOf() default {}; } @BinderThread @@ -132,13 +145,17 @@ final class IInputMethodManagerImpl extends IInputMethodManager.Stub { void showInputMethodPickerFromClient(IInputMethodClient client, int auxiliarySubtypeMode); - @PermissionVerified(Manifest.permission.WRITE_SECURE_SETTINGS) + @PermissionVerified(allOf = { + Manifest.permission.INTERACT_ACROSS_USERS_FULL, + Manifest.permission.WRITE_SECURE_SETTINGS}) void showInputMethodPickerFromSystem(int auxiliarySubtypeMode, int displayId); @PermissionVerified(Manifest.permission.TEST_INPUT_METHOD) boolean isInputMethodPickerShownForTest(); - @PermissionVerified(Manifest.permission.WRITE_SECURE_SETTINGS) + @PermissionVerified(allOf = { + Manifest.permission.INTERACT_ACROSS_USERS_FULL, + Manifest.permission.WRITE_SECURE_SETTINGS}) void onImeSwitchButtonClickFromSystem(int displayId); InputMethodSubtype getCurrentInputMethodSubtype(@UserIdInt int userId); @@ -153,7 +170,9 @@ final class IInputMethodManagerImpl extends IInputMethodManager.Stub { void reportPerceptibleAsync(IBinder windowToken, boolean perceptible); - @PermissionVerified(Manifest.permission.INTERNAL_SYSTEM_WINDOW) + @PermissionVerified(allOf = { + Manifest.permission.INTERACT_ACROSS_USERS_FULL, + Manifest.permission.INTERNAL_SYSTEM_WINDOW}) void removeImeSurface(int displayId); void removeImeSurfaceFromWindowAsync(IBinder windowToken); @@ -330,13 +349,14 @@ final class IInputMethodManagerImpl extends IInputMethodManager.Stub { mCallback.showInputMethodPickerFromClient(client, auxiliarySubtypeMode); } - @EnforcePermission(Manifest.permission.WRITE_SECURE_SETTINGS) + @EnforcePermission(allOf = { + Manifest.permission.WRITE_SECURE_SETTINGS, + Manifest.permission.INTERACT_ACROSS_USERS_FULL}) @Override public void showInputMethodPickerFromSystem(int auxiliarySubtypeMode, int displayId) { super.showInputMethodPickerFromSystem_enforcePermission(); mCallback.showInputMethodPickerFromSystem(auxiliarySubtypeMode, displayId); - } @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD) @@ -347,7 +367,9 @@ final class IInputMethodManagerImpl extends IInputMethodManager.Stub { return mCallback.isInputMethodPickerShownForTest(); } - @EnforcePermission(Manifest.permission.WRITE_SECURE_SETTINGS) + @EnforcePermission(allOf = { + Manifest.permission.WRITE_SECURE_SETTINGS, + Manifest.permission.INTERACT_ACROSS_USERS_FULL}) @Override public void onImeSwitchButtonClickFromSystem(int displayId) { super.onImeSwitchButtonClickFromSystem_enforcePermission(); @@ -382,7 +404,9 @@ final class IInputMethodManagerImpl extends IInputMethodManager.Stub { mCallback.reportPerceptibleAsync(windowToken, perceptible); } - @EnforcePermission(Manifest.permission.INTERNAL_SYSTEM_WINDOW) + @EnforcePermission(allOf = { + Manifest.permission.INTERNAL_SYSTEM_WINDOW, + Manifest.permission.INTERACT_ACROSS_USERS_FULL}) @Override public void removeImeSurface(int displayId) { super.removeImeSurface_enforcePermission(); diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java index 0b3f3f0b979a..42a99defcbee 100644 --- a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java +++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java @@ -121,9 +121,9 @@ public final class ImeVisibilityStateComputer { @GuardedBy("ImfLock.class") private boolean mRequestedImeScreenshot; - /** The window token of the current visible IME layering target overlay. */ + /** Whether there is a visible IME layering target overlay. */ @GuardedBy("ImfLock.class") - private IBinder mCurVisibleImeLayeringOverlay; + private boolean mHasVisibleImeLayeringOverlay; /** The window token of the current visible IME input target. */ @GuardedBy("ImfLock.class") @@ -218,33 +218,36 @@ public final class ImeVisibilityStateComputer { mPolicy = imePolicy; mWindowManagerInternal.setInputMethodTargetChangeListener(new ImeTargetChangeListener() { @Override - public void onImeTargetOverlayVisibilityChanged(IBinder overlayWindowToken, + public void onImeTargetOverlayVisibilityChanged(@NonNull IBinder overlayWindowToken, @WindowManager.LayoutParams.WindowType int windowType, boolean visible, boolean removed) { // Ignoring the starting window since it's ok to cover the IME target // window in temporary without affecting the IME visibility. - final var overlay = (visible && !removed && windowType != TYPE_APPLICATION_STARTING) - ? overlayWindowToken : null; + final boolean hasOverlay = visible && !removed + && windowType != TYPE_APPLICATION_STARTING; synchronized (ImfLock.class) { - mCurVisibleImeLayeringOverlay = overlay; + mHasVisibleImeLayeringOverlay = hasOverlay; } } @Override public void onImeInputTargetVisibilityChanged(IBinder imeInputTarget, boolean visibleRequested, boolean removed) { + final boolean visibleAndNotRemoved = visibleRequested && !removed; synchronized (ImfLock.class) { - if (mCurVisibleImeInputTarget == imeInputTarget && (!visibleRequested - || removed) - && mCurVisibleImeLayeringOverlay != null) { + if (visibleAndNotRemoved) { + mCurVisibleImeInputTarget = imeInputTarget; + return; + } + if (mHasVisibleImeLayeringOverlay + && mCurVisibleImeInputTarget == imeInputTarget) { final int reason = SoftInputShowHideReason.HIDE_WHEN_INPUT_TARGET_INVISIBLE; final var statsToken = ImeTracker.forLogging().onStart(ImeTracker.TYPE_HIDE, ImeTracker.ORIGIN_SERVER, reason, false /* fromUser */); mService.onApplyImeVisibilityFromComputerLocked(imeInputTarget, statsToken, new ImeVisibilityResult(STATE_HIDE_IME_EXPLICIT, reason)); } - mCurVisibleImeInputTarget = - (visibleRequested && !removed) ? imeInputTarget : null; + mCurVisibleImeInputTarget = null; } } }); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java index 13209d861e8b..dba04656e48f 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java @@ -75,13 +75,13 @@ public abstract class InputMethodManagerInternal { public abstract void setInteractive(boolean interactive); /** - * Hides the input methods for all the users, if visible. + * Hides the input method for the specified {@code originatingDisplayId}, if visible. * * @param reason the reason for hiding the current input method * @param originatingDisplayId the display ID the request is originated */ @ImfLockFree - public abstract void hideAllInputMethods(@SoftInputShowHideReason int reason, + public abstract void hideInputMethod(@SoftInputShowHideReason int reason, int originatingDisplayId); /** @@ -315,7 +315,7 @@ public abstract class InputMethodManagerInternal { @ImfLockFree @Override - public void hideAllInputMethods(@SoftInputShowHideReason int reason, + public void hideInputMethod(@SoftInputShowHideReason int reason, int originatingDisplayId) { } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 7ff03c2060de..5e7d39196d46 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -254,7 +254,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. private @interface MultiUserUnawareField { } - private static final int MSG_HIDE_ALL_INPUT_METHODS = 1035; + private static final int MSG_HIDE_INPUT_METHOD = 1035; private static final int MSG_REMOVE_IME_SURFACE = 1060; private static final int MSG_REMOVE_IME_SURFACE_FROM_WINDOW = 1061; @@ -997,8 +997,6 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. Process.THREAD_PRIORITY_FOREGROUND, true /* allowIo */); ioThread.start(); - SecureSettingsWrapper.setContentResolver(context.getContentResolver()); - return new InputMethodManagerService(context, shouldEnableConcurrentMultiUserMode(context), thread.getLooper(), Handler.createAsync(ioThread.getLooper()), @@ -1057,7 +1055,6 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. public void onUserRemoved(UserInfo user) { // Called directly from UserManagerService. Do not block the calling thread. final int userId = user.id; - SecureSettingsWrapper.onUserRemoved(userId); AdditionalSubtypeMapRepository.remove(userId); InputMethodSettingsRepository.remove(userId); mService.mUserDataRepository.remove(userId); @@ -1129,6 +1126,21 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } }); } + + @Override + public void onUserStopped(@NonNull TargetUser user) { + final int userId = user.getUserIdentifier(); + // Called on ActivityManager thread. + SecureSettingsWrapper.onUserStopped(userId); + mService.mIoHandler.post(() -> { + final var additionalSubtypeMap = AdditionalSubtypeMapRepository.get(userId); + final var settings = InputMethodManagerService.queryInputMethodServicesInternal( + mService.mContext, userId, additionalSubtypeMap, + DirectBootAwareness.AUTO).getMethodMap(); + InputMethodSettingsRepository.put(userId, + InputMethodSettings.create(settings, userId)); + }); + } } @GuardedBy("ImfLock.class") @@ -1163,6 +1175,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. mConcurrentMultiUserModeEnabled = concurrentMultiUserModeEnabled; mContext = context; mRes = context.getResources(); + SecureSettingsWrapper.onStart(mContext); mHandler = Handler.createAsync(uiLooper, this); mIoHandler = ioHandler; @@ -1846,13 +1859,6 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } } - @VisibleForTesting - void setAttachedClientForTesting(@NonNull ClientState cs) { - synchronized (ImfLock.class) { - getUserData(mCurrentUserId).mCurClient = cs; - } - } - @GuardedBy("ImfLock.class") private boolean isShowRequestedForCurrentWindow(@UserIdInt int userId) { final var userData = getUserData(userId); @@ -3996,7 +4002,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } } - @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.WRITE_SECURE_SETTINGS) + @IInputMethodManagerImpl.PermissionVerified(allOf = { + Manifest.permission.INTERACT_ACROSS_USERS_FULL, + Manifest.permission.WRITE_SECURE_SETTINGS}) @Override public void showInputMethodPickerFromSystem(int auxiliarySubtypeMode, int displayId) { // Always call subtype picker, because subtype picker is a superset of input method @@ -4090,7 +4098,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } } - @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.WRITE_SECURE_SETTINGS) + @IInputMethodManagerImpl.PermissionVerified(allOf = { + Manifest.permission.INTERACT_ACROSS_USERS_FULL, + Manifest.permission.WRITE_SECURE_SETTINGS}) @Override public void onImeSwitchButtonClickFromSystem(int displayId) { synchronized (ImfLock.class) { @@ -4432,7 +4442,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. }); } - @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.INTERNAL_SYSTEM_WINDOW) + @IInputMethodManagerImpl.PermissionVerified(allOf = { + Manifest.permission.INTERACT_ACROSS_USERS_FULL, + Manifest.permission.INTERNAL_SYSTEM_WINDOW}) @Override public void removeImeSurface(int displayId) { mHandler.obtainMessage(MSG_REMOVE_IME_SURFACE).sendToTarget(); @@ -5034,7 +5046,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. return; } - if (Flags.imeSwitcherRevamp()) { + if (mNewInputMethodSwitcherMenuEnabled) { if (DEBUG) { Slog.v(TAG, "Show IME switcher menu," + " showAuxSubtypes=" + showAuxSubtypes @@ -5066,7 +5078,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @Override public boolean handleMessage(Message msg) { switch (msg.what) { - case MSG_HIDE_ALL_INPUT_METHODS: { + case MSG_HIDE_INPUT_METHOD: { @SoftInputShowHideReason final int reason = msg.arg1; final int originatingDisplayId = msg.arg2; synchronized (ImfLock.class) { @@ -5802,10 +5814,10 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @ImfLockFree @Override - public void hideAllInputMethods(@SoftInputShowHideReason int reason, + public void hideInputMethod(@SoftInputShowHideReason int reason, int originatingDisplayId) { - mHandler.removeMessages(MSG_HIDE_ALL_INPUT_METHODS); - mHandler.obtainMessage(MSG_HIDE_ALL_INPUT_METHODS, reason, originatingDisplayId) + mHandler.removeMessages(MSG_HIDE_INPUT_METHOD); + mHandler.obtainMessage(MSG_HIDE_INPUT_METHOD, reason, originatingDisplayId) .sendToTarget(); } diff --git a/services/core/java/com/android/server/inputmethod/SecureSettingsWrapper.java b/services/core/java/com/android/server/inputmethod/SecureSettingsWrapper.java index b3500be054f9..476888ebf26d 100644 --- a/services/core/java/com/android/server/inputmethod/SecureSettingsWrapper.java +++ b/services/core/java/com/android/server/inputmethod/SecureSettingsWrapper.java @@ -20,7 +20,10 @@ import android.annotation.AnyThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; +import android.app.ActivityManagerInternal; import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.UserInfo; import android.provider.Settings; import android.util.ArrayMap; import android.util.ArraySet; @@ -318,13 +321,30 @@ final class SecureSettingsWrapper { } /** - * Called when the system is starting. + * Called when {@link InputMethodManagerService} is starting. * - * @param contentResolver the {@link ContentResolver} to be used + * @param context the {@link Context} to be used. */ @AnyThread - static void setContentResolver(@NonNull ContentResolver contentResolver) { - sContentResolver = contentResolver; + static void onStart(@NonNull Context context) { + sContentResolver = context.getContentResolver(); + + final int userId = LocalServices.getService(ActivityManagerInternal.class) + .getCurrentUserId(); + final UserManagerInternal userManagerInternal = + LocalServices.getService(UserManagerInternal.class); + putOrGet(userId, createImpl(userManagerInternal, userId)); + + userManagerInternal.addUserLifecycleListener( + new UserManagerInternal.UserLifecycleListener() { + @Override + public void onUserRemoved(UserInfo user) { + synchronized (sMutationLock) { + sUserMap = sUserMap.cloneWithRemoveOrSelf(user.id); + } + } + } + ); } /** @@ -357,14 +377,19 @@ final class SecureSettingsWrapper { } /** - * Called when a user is being removed. + * Called when a user is stopped, which changes the user storage to the locked state again. * - * @param userId the ID of the user whose storage is being removed. + * @param userId the ID of the user whose storage is being locked again. */ @AnyThread - static void onUserRemoved(@UserIdInt int userId) { + static void onUserStopped(@UserIdInt int userId) { + final LockedUserImpl lockedUserImpl = new LockedUserImpl(userId, sContentResolver); synchronized (sMutationLock) { - sUserMap = sUserMap.cloneWithRemoveOrSelf(userId); + final ReaderWriter current = sUserMap.get(userId); + if (current == null || current instanceof LockedUserImpl) { + return; + } + sUserMap = sUserMap.cloneWithPutOrSelf(userId, lockedUserImpl); } } diff --git a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java index f603ff3d7417..c940a9cd7b81 100644 --- a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java +++ b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java @@ -252,7 +252,9 @@ final class ZeroJankProxy implements IInputMethodManagerImpl.Callback { offload(() -> mInner.showInputMethodPickerFromClient(client, auxiliarySubtypeMode)); } - @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.WRITE_SECURE_SETTINGS) + @IInputMethodManagerImpl.PermissionVerified(allOf = { + Manifest.permission.INTERACT_ACROSS_USERS_FULL, + Manifest.permission.WRITE_SECURE_SETTINGS}) @Override public void showInputMethodPickerFromSystem(int auxiliarySubtypeMode, int displayId) { mInner.showInputMethodPickerFromSystem(auxiliarySubtypeMode, displayId); @@ -264,7 +266,9 @@ final class ZeroJankProxy implements IInputMethodManagerImpl.Callback { return mInner.isInputMethodPickerShownForTest(); } - @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.WRITE_SECURE_SETTINGS) + @IInputMethodManagerImpl.PermissionVerified(allOf = { + Manifest.permission.INTERACT_ACROSS_USERS_FULL, + Manifest.permission.WRITE_SECURE_SETTINGS}) @Override public void onImeSwitchButtonClickFromSystem(int displayId) { mInner.onImeSwitchButtonClickFromSystem(displayId); @@ -298,7 +302,9 @@ final class ZeroJankProxy implements IInputMethodManagerImpl.Callback { mInner.reportPerceptibleAsync(windowToken, perceptible); } - @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.INTERNAL_SYSTEM_WINDOW) + @IInputMethodManagerImpl.PermissionVerified(allOf = { + Manifest.permission.INTERACT_ACROSS_USERS_FULL, + Manifest.permission.INTERNAL_SYSTEM_WINDOW}) @Override public void removeImeSurface(int displayId) { mInner.removeImeSurface(displayId); diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java index 1a8e44b526dd..1fdb57c0b61a 100644 --- a/services/core/java/com/android/server/notification/ManagedServices.java +++ b/services/core/java/com/android/server/notification/ManagedServices.java @@ -950,7 +950,7 @@ abstract public class ManagedServices { || isPackageOrComponentAllowed(component.getPackageName(), userId))) { return false; } - return componentHasBindPermission(component, userId); + return isValidService(component, userId); } private boolean componentHasBindPermission(ComponentName component, int userId) { @@ -1302,11 +1302,12 @@ abstract public class ManagedServices { if (TextUtils.equals(getPackageName(approvedPackageOrComponent), packageName)) { final ComponentName component = ComponentName.unflattenFromString( approvedPackageOrComponent); - if (component != null && !componentHasBindPermission(component, userId)) { + if (component != null && !isValidService(component, userId)) { approved.removeAt(j); if (DEBUG) { Slog.v(TAG, "Removing " + approvedPackageOrComponent - + " from approved list; no bind permission found " + + " from approved list; no bind permission or " + + "service interface filter found " + mConfig.bindPermission); } } @@ -1325,6 +1326,11 @@ abstract public class ManagedServices { } } + protected boolean isValidService(ComponentName component, int userId) { + return componentHasBindPermission(component, userId) && queryPackageForServices( + component.getPackageName(), userId).contains(component); + } + protected boolean isValidEntry(String packageOrComponent, int userId) { return hasMatchingServices(packageOrComponent, userId); } diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 44250790619d..a0d5ea875abf 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -501,9 +501,9 @@ final class InstallPackageHelper { mPm.setUpCustomResolverActivity(pkg, pkgSetting); } - // When upgrading a package, pkgSetting is copied from oldPkgSetting. Clear the app - // metadata file path for the new package. - if (oldPkgSetting != null) { + // When upgrading a package, clear the app metadata file path for the new package. + if (oldPkgSetting != null + && oldPkgSetting.getLastUpdateTime() < pkgSetting.getLastUpdateTime()) { pkgSetting.setAppMetadataFilePath(null); pkgSetting.setAppMetadataSource(APP_METADATA_SOURCE_UNKNOWN); } diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 7534bfe7b8ee..d0706d22a773 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -1158,8 +1158,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { break; case SHORT_PRESS_POWER_CLOSE_IME_OR_GO_HOME: { if (mDismissImeOnBackKeyPressed) { - // TODO(b/308479256): Check if hiding "all" IMEs is OK or not. - InputMethodManagerInternal.get().hideAllInputMethods( + InputMethodManagerInternal.get().hideInputMethod( SoftInputShowHideReason.HIDE_POWER_BUTTON_GO_HOME, displayId); } else { shortPressPowerGoHome(); diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index 10faf1455995..ecb0c30b13e4 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -34,6 +34,7 @@ import static android.os.PowerManagerInternal.wakefulnessToString; import static com.android.internal.util.LatencyTracker.ACTION_TURN_ON_SCREEN; import static com.android.server.deviceidle.Flags.disableWakelocksInLightIdle; import static com.android.server.display.DisplayDeviceConfig.INVALID_BRIGHTNESS_IN_CONFIG; +import static com.android.server.display.brightness.BrightnessUtils.isValidBrightnessValue; import android.annotation.IntDef; import android.annotation.NonNull; @@ -650,11 +651,16 @@ public final class PowerManagerService extends SystemService private int mDozeScreenStateOverrideReasonFromDreamManager = Display.STATE_REASON_UNKNOWN; - // The screen brightness to use while dozing. + // The screen brightness between 1 and 255 to use while dozing. private int mDozeScreenBrightnessOverrideFromDreamManager = PowerManager.BRIGHTNESS_DEFAULT; + /** + * The screen brightness between {@link PowerManager#BRIGHTNESS_MIN} and + * {@link PowerManager.BRIGHTNESS_MAX} to use while dozing. + */ private float mDozeScreenBrightnessOverrideFromDreamManagerFloat = PowerManager.BRIGHTNESS_INVALID_FLOAT; + // Keep display state when dozing. private boolean mDrawWakeLockOverrideFromSidekick; @@ -4455,15 +4461,21 @@ public final class PowerManagerService extends SystemService } private void setDozeOverrideFromDreamManagerInternal( - int screenState, @Display.StateReason int reason, int screenBrightness) { + int screenState, @Display.StateReason int reason, float screenBrightnessFloat, + int screenBrightnessInt) { synchronized (mLock) { if (mDozeScreenStateOverrideFromDreamManager != screenState - || mDozeScreenBrightnessOverrideFromDreamManager != screenBrightness) { + || mDozeScreenBrightnessOverrideFromDreamManager != screenBrightnessInt + || !BrightnessSynchronizer.floatEquals( + mDozeScreenBrightnessOverrideFromDreamManagerFloat, + screenBrightnessFloat)) { mDozeScreenStateOverrideFromDreamManager = screenState; mDozeScreenStateOverrideReasonFromDreamManager = reason; - mDozeScreenBrightnessOverrideFromDreamManager = screenBrightness; + mDozeScreenBrightnessOverrideFromDreamManager = screenBrightnessInt; mDozeScreenBrightnessOverrideFromDreamManagerFloat = - BrightnessSynchronizer.brightnessIntToFloat(mDozeScreenBrightnessOverrideFromDreamManager); + isValidBrightnessValue(screenBrightnessFloat) + ? screenBrightnessFloat + : BrightnessSynchronizer.brightnessIntToFloat(screenBrightnessInt); mDirty |= DIRTY_SETTINGS; updatePowerStateLocked(); } @@ -7095,7 +7107,7 @@ public final class PowerManagerService extends SystemService @Override public void setDozeOverrideFromDreamManager( - int screenState, int reason, int screenBrightness) { + int screenState, int reason, float screenBrightnessFloat, int screenBrightnessInt) { switch (screenState) { case Display.STATE_UNKNOWN: case Display.STATE_OFF: @@ -7108,11 +7120,17 @@ public final class PowerManagerService extends SystemService screenState = Display.STATE_UNKNOWN; break; } - if (screenBrightness < PowerManager.BRIGHTNESS_DEFAULT - || screenBrightness > PowerManager.BRIGHTNESS_ON) { - screenBrightness = PowerManager.BRIGHTNESS_DEFAULT; + if (screenBrightnessInt < PowerManager.BRIGHTNESS_DEFAULT + || screenBrightnessInt > PowerManager.BRIGHTNESS_ON) { + screenBrightnessInt = PowerManager.BRIGHTNESS_DEFAULT; + } + if (screenBrightnessFloat != PowerManager.BRIGHTNESS_OFF_FLOAT + && (screenBrightnessFloat < PowerManager.BRIGHTNESS_MIN + || screenBrightnessFloat > PowerManager.BRIGHTNESS_MAX)) { + screenBrightnessFloat = PowerManager.BRIGHTNESS_INVALID_FLOAT; } - setDozeOverrideFromDreamManagerInternal(screenState, reason, screenBrightness); + setDozeOverrideFromDreamManagerInternal(screenState, reason, screenBrightnessFloat, + screenBrightnessInt); } @Override diff --git a/services/core/java/com/android/server/power/hint/TEST_MAPPING b/services/core/java/com/android/server/power/hint/TEST_MAPPING index 99887863b885..545070050977 100644 --- a/services/core/java/com/android/server/power/hint/TEST_MAPPING +++ b/services/core/java/com/android/server/power/hint/TEST_MAPPING @@ -1,5 +1,5 @@ { - "postsubmit": [ + "presubmit": [ { "name": "PerformanceHintTests", "options": [ diff --git a/services/core/java/com/android/server/power/stats/PowerStatsExporter.java b/services/core/java/com/android/server/power/stats/PowerStatsExporter.java index 39954b8e8dba..5f41090da0a4 100644 --- a/services/core/java/com/android/server/power/stats/PowerStatsExporter.java +++ b/services/core/java/com/android/server/power/stats/PowerStatsExporter.java @@ -59,58 +59,61 @@ public class PowerStatsExporter { */ public void exportAggregatedPowerStats(BatteryUsageStats.Builder batteryUsageStatsBuilder, long monotonicStartTime, long monotonicEndTime) { - boolean hasStoredSpans = false; - long maxEndTime = monotonicStartTime; - List<PowerStatsSpan.Metadata> spans = mPowerStatsStore.getTableOfContents(); - for (int i = spans.size() - 1; i >= 0; i--) { - PowerStatsSpan.Metadata metadata = spans.get(i); - if (!metadata.getSections().contains(AggregatedPowerStatsSection.TYPE)) { - continue; - } + synchronized (this) { + boolean hasStoredSpans = false; + long maxEndTime = monotonicStartTime; + List<PowerStatsSpan.Metadata> spans = mPowerStatsStore.getTableOfContents(); + for (int i = spans.size() - 1; i >= 0; i--) { + PowerStatsSpan.Metadata metadata = spans.get(i); + if (!metadata.getSections().contains(AggregatedPowerStatsSection.TYPE)) { + continue; + } - List<PowerStatsSpan.TimeFrame> timeFrames = metadata.getTimeFrames(); - long spanMinTime = Long.MAX_VALUE; - long spanMaxTime = Long.MIN_VALUE; - for (int j = 0; j < timeFrames.size(); j++) { - PowerStatsSpan.TimeFrame timeFrame = timeFrames.get(j); - long startMonotonicTime = timeFrame.startMonotonicTime; - long endMonotonicTime = startMonotonicTime + timeFrame.duration; - if (startMonotonicTime < spanMinTime) { - spanMinTime = startMonotonicTime; + List<PowerStatsSpan.TimeFrame> timeFrames = metadata.getTimeFrames(); + long spanMinTime = Long.MAX_VALUE; + long spanMaxTime = Long.MIN_VALUE; + for (int j = 0; j < timeFrames.size(); j++) { + PowerStatsSpan.TimeFrame timeFrame = timeFrames.get(j); + long startMonotonicTime = timeFrame.startMonotonicTime; + long endMonotonicTime = startMonotonicTime + timeFrame.duration; + if (startMonotonicTime < spanMinTime) { + spanMinTime = startMonotonicTime; + } + if (endMonotonicTime > spanMaxTime) { + spanMaxTime = endMonotonicTime; + } } - if (endMonotonicTime > spanMaxTime) { - spanMaxTime = endMonotonicTime; + + if (!(spanMinTime >= monotonicStartTime && spanMaxTime < monotonicEndTime)) { + continue; } - } - if (!(spanMinTime >= monotonicStartTime && spanMaxTime < monotonicEndTime)) { - continue; - } + if (spanMaxTime > maxEndTime) { + maxEndTime = spanMaxTime; + } - if (spanMaxTime > maxEndTime) { - maxEndTime = spanMaxTime; + PowerStatsSpan span = mPowerStatsStore.loadPowerStatsSpan(metadata.getId(), + AggregatedPowerStatsSection.TYPE); + if (span == null) { + Slog.e(TAG, "Could not read PowerStatsStore section " + metadata); + continue; + } + List<PowerStatsSpan.Section> sections = span.getSections(); + for (int k = 0; k < sections.size(); k++) { + hasStoredSpans = true; + PowerStatsSpan.Section section = sections.get(k); + populateBatteryUsageStatsBuilder(batteryUsageStatsBuilder, + ((AggregatedPowerStatsSection) section).getAggregatedPowerStats()); + } } - PowerStatsSpan span = mPowerStatsStore.loadPowerStatsSpan(metadata.getId(), - AggregatedPowerStatsSection.TYPE); - if (span == null) { - Slog.e(TAG, "Could not read PowerStatsStore section " + metadata); - continue; - } - List<PowerStatsSpan.Section> sections = span.getSections(); - for (int k = 0; k < sections.size(); k++) { - hasStoredSpans = true; - PowerStatsSpan.Section section = sections.get(k); - populateBatteryUsageStatsBuilder(batteryUsageStatsBuilder, - ((AggregatedPowerStatsSection) section).getAggregatedPowerStats()); + if (!hasStoredSpans + || maxEndTime < monotonicEndTime - mBatterySessionTimeSpanSlackMillis) { + mPowerStatsAggregator.aggregatePowerStats(maxEndTime, monotonicEndTime, + stats -> populateBatteryUsageStatsBuilder(batteryUsageStatsBuilder, stats)); } + mPowerStatsAggregator.reset(); } - - if (!hasStoredSpans || maxEndTime < monotonicEndTime - mBatterySessionTimeSpanSlackMillis) { - mPowerStatsAggregator.aggregatePowerStats(maxEndTime, monotonicEndTime, - stats -> populateBatteryUsageStatsBuilder(batteryUsageStatsBuilder, stats)); - } - mPowerStatsAggregator.reset(); } private void populateBatteryUsageStatsBuilder( diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 85c8900555d9..e9423ce48624 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -1890,8 +1890,7 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D enforceStatusBarService(); final long token = Binder.clearCallingIdentity(); try { - // TODO(b/308479256): Check if hiding "all" IMEs is OK or not. - InputMethodManagerInternal.get().hideAllInputMethods( + InputMethodManagerInternal.get().hideInputMethod( SoftInputShowHideReason.HIDE_BUBBLES, displayId); } finally { Binder.restoreCallingIdentity(token); diff --git a/services/core/java/com/android/server/vibrator/HalVibration.java b/services/core/java/com/android/server/vibrator/HalVibration.java index f9bad59d38b0..46bd7af159da 100644 --- a/services/core/java/com/android/server/vibrator/HalVibration.java +++ b/services/core/java/com/android/server/vibrator/HalVibration.java @@ -108,23 +108,6 @@ final class HalVibration extends Vibration { } /** - * Resolves the default vibration amplitude of {@link #getEffectToPlay()} and each fallback. - * - * @param defaultAmplitude An integer in [1,255] representing the device default amplitude to - * replace the {@link VibrationEffect#DEFAULT_AMPLITUDE}. - */ - public void resolveEffects(int defaultAmplitude) { - CombinedVibration newEffect = - mEffectToPlay.transform(VibrationEffect::resolve, defaultAmplitude); - if (!Objects.equals(mEffectToPlay, newEffect)) { - mEffectToPlay = newEffect; - } - for (int i = 0; i < mFallbacks.size(); i++) { - mFallbacks.setValueAt(i, mFallbacks.valueAt(i).resolve(defaultAmplitude)); - } - } - - /** * Scales the {@link #getEffectToPlay()} and each fallback effect based on the vibration usage. */ public void scaleEffects(VibrationScaler scaler) { diff --git a/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java index 98a2ba0d62cf..3f9da82e3d2e 100644 --- a/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java +++ b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java @@ -46,6 +46,8 @@ public final class HapticFeedbackVibrationProvider { VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK); private static final VibrationAttributes COMMUNICATION_REQUEST_VIBRATION_ATTRIBUTES = VibrationAttributes.createForUsage(VibrationAttributes.USAGE_COMMUNICATION_REQUEST); + private static final VibrationAttributes IME_FEEDBACK_VIBRATION_ATTRIBUTES = + VibrationAttributes.createForUsage(VibrationAttributes.USAGE_IME_FEEDBACK); private final VibratorInfo mVibratorInfo; private final boolean mHapticTextHandleEnabled; @@ -219,8 +221,6 @@ public final class HapticFeedbackVibrationProvider { } int vibFlags = 0; - boolean fromIme = - (privFlags & HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS) != 0; boolean bypassVibrationIntensitySetting = (flags & HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING) != 0; if (bypassVibrationIntensitySetting) { @@ -229,9 +229,6 @@ public final class HapticFeedbackVibrationProvider { if (shouldBypassInterruptionPolicy(effectId)) { vibFlags |= VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY; } - if (shouldBypassIntensityScale(effectId, fromIme)) { - vibFlags |= VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE; - } return vibFlags == 0 ? attrs : new VibrationAttributes.Builder(attrs) .setFlags(vibFlags).build(); @@ -362,22 +359,6 @@ public final class HapticFeedbackVibrationProvider { /* fallbackForPredefinedEffect= */ predefinedEffectFallback); } - private boolean shouldBypassIntensityScale(int effectId, boolean isIme) { - if (!Flags.keyboardCategoryEnabled() || mKeyboardVibrationFixedAmplitude < 0 || !isIme) { - // Shouldn't bypass if not support keyboard category, no fixed amplitude or not an IME. - return false; - } - switch (effectId) { - case HapticFeedbackConstants.KEYBOARD_TAP: - return mVibratorInfo.isPrimitiveSupported( - VibrationEffect.Composition.PRIMITIVE_CLICK); - case HapticFeedbackConstants.KEYBOARD_RELEASE: - return mVibratorInfo.isPrimitiveSupported( - VibrationEffect.Composition.PRIMITIVE_TICK); - } - return false; - } - private VibrationAttributes createKeyboardVibrationAttributes( @HapticFeedbackConstants.PrivateFlags int privFlags) { // Use touch attribute when the keyboard category is disable. @@ -388,7 +369,8 @@ public final class HapticFeedbackVibrationProvider { if ((privFlags & HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS) == 0) { return TOUCH_VIBRATION_ATTRIBUTES; } - return new VibrationAttributes.Builder(TOUCH_VIBRATION_ATTRIBUTES) + return new VibrationAttributes.Builder(IME_FEEDBACK_VIBRATION_ATTRIBUTES) + // TODO(b/332661766): Remove CATEGORY_KEYBOARD logic .setCategory(VibrationAttributes.CATEGORY_KEYBOARD) .build(); } diff --git a/services/core/java/com/android/server/vibrator/VibrationSettings.java b/services/core/java/com/android/server/vibrator/VibrationSettings.java index 02061555a37f..fb92d609f1cf 100644 --- a/services/core/java/com/android/server/vibrator/VibrationSettings.java +++ b/services/core/java/com/android/server/vibrator/VibrationSettings.java @@ -21,6 +21,7 @@ import static android.os.VibrationAttributes.USAGE_ACCESSIBILITY; import static android.os.VibrationAttributes.USAGE_ALARM; import static android.os.VibrationAttributes.USAGE_COMMUNICATION_REQUEST; import static android.os.VibrationAttributes.USAGE_HARDWARE_FEEDBACK; +import static android.os.VibrationAttributes.USAGE_IME_FEEDBACK; import static android.os.VibrationAttributes.USAGE_MEDIA; import static android.os.VibrationAttributes.USAGE_NOTIFICATION; import static android.os.VibrationAttributes.USAGE_PHYSICAL_EMULATION; @@ -560,6 +561,7 @@ final class VibrationSettings { mKeyboardVibrationOn = loadSystemSetting( Settings.System.KEYBOARD_VIBRATION_ENABLED, 1, userHandle) > 0; + int keyboardIntensity = getDefaultIntensity(USAGE_IME_FEEDBACK); int alarmIntensity = toIntensity( loadSystemSetting(Settings.System.ALARM_VIBRATION_INTENSITY, -1, userHandle), getDefaultIntensity(USAGE_ALARM)); @@ -610,6 +612,12 @@ final class VibrationSettings { mCurrentVibrationIntensities.put(USAGE_TOUCH, hapticFeedbackIntensity); } + if (mVibrationConfig.isKeyboardVibrationSettingsSupported()) { + mCurrentVibrationIntensities.put(USAGE_IME_FEEDBACK, keyboardIntensity); + } else { + mCurrentVibrationIntensities.put(USAGE_IME_FEEDBACK, hapticFeedbackIntensity); + } + // A11y is not disabled by any haptic feedback setting. mCurrentVibrationIntensities.put(USAGE_ACCESSIBILITY, positiveHapticFeedbackIntensity); } diff --git a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java index 8c9a92de03a9..7152844cc772 100644 --- a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java +++ b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java @@ -21,7 +21,6 @@ import android.annotation.Nullable; import android.os.Build; import android.os.CombinedVibration; import android.os.IBinder; -import android.os.VibrationAttributes; import android.os.VibrationEffect; import android.os.vibrator.Flags; import android.os.vibrator.PrebakedSegment; @@ -177,16 +176,11 @@ final class VibrationStepConductor implements IBinder.DeathRecipient { expectIsVibrationThread(true); } - if (!mVibration.callerInfo.attrs.isFlagSet( - VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)) { - if (Flags.adaptiveHapticsEnabled()) { - waitForVibrationParamsIfRequired(); - } - // Scale resolves the default amplitudes from the effect before scaling them. - mVibration.scaleEffects(mVibrationScaler); - } else { - mVibration.resolveEffects(mVibrationScaler.getDefaultVibrationAmplitude()); + if (Flags.adaptiveHapticsEnabled()) { + waitForVibrationParamsIfRequired(); } + // Scale resolves the default amplitudes from the effect before scaling them. + mVibration.scaleEffects(mVibrationScaler); mVibration.adaptToDevice(mDeviceAdapter); CombinedVibration.Sequential sequentialEffect = toSequential(mVibration.getEffectToPlay()); diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index 48c4a68250b1..7610d7d6b659 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -103,8 +103,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { new VibrationAttributes.Builder().build(); private static final int ATTRIBUTES_ALL_BYPASS_FLAGS = VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY - | VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF - | VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE; + | VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF; /** Fixed large duration used to note repeating vibrations to {@link IBatteryStats}. */ private static final long BATTERY_STATS_REPEATING_VIBRATION_DURATION = 5_000; @@ -925,8 +924,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { private VibrationStepConductor createVibrationStepConductor(HalVibration vib) { CompletableFuture<Void> requestVibrationParamsFuture = null; - if (Flags.adaptiveHapticsEnabled() && !vib.callerInfo.attrs.isFlagSet( - VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE) + if (Flags.adaptiveHapticsEnabled() && mVibratorControlService.shouldRequestVibrationParams( vib.callerInfo.attrs.getUsage())) { requestVibrationParamsFuture = @@ -940,13 +938,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } private Vibration.EndInfo startVibrationOnInputDevicesLocked(HalVibration vib) { - if (!vib.callerInfo.attrs.isFlagSet( - VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)) { - // Scale resolves the default amplitudes from the effect before scaling them. - vib.scaleEffects(mVibrationScaler); - } else { - vib.resolveEffects(mVibrationScaler.getDefaultVibrationAmplitude()); - } + // Scale resolves the default amplitudes from the effect before scaling them. + vib.scaleEffects(mVibrationScaler); mInputDeviceDelegate.vibrateIfAvailable(vib.callerInfo, vib.getEffectToPlay()); return new Vibration.EndInfo(Vibration.Status.FORWARDED_TO_INPUT_DEVICES); diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java index f70a3ba107e1..d5bea4adaf8c 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java @@ -676,7 +676,12 @@ public class WallpaperCropper { final Rect estimateCrop = new Rect(cropHint); if (!multiCrop()) estimateCrop.scale(1f / options.inSampleSize); - else estimateCrop.scale(1f / sampleSize); + else { + estimateCrop.left = (int) Math.floor(estimateCrop.left / sampleSize); + estimateCrop.top = (int) Math.floor(estimateCrop.top / sampleSize); + estimateCrop.right = (int) Math.ceil(estimateCrop.right / sampleSize); + estimateCrop.bottom = (int) Math.ceil(estimateCrop.bottom / sampleSize); + } float hRatio = (float) wpData.mHeight / estimateCrop.height(); final int destHeight = (int) (estimateCrop.height() * hRatio); final int destWidth = (int) (estimateCrop.width() * hRatio); @@ -720,7 +725,10 @@ public class WallpaperCropper { } if (multiCrop()) { Slog.v(TAG, " cropHint=" + cropHint); + Slog.v(TAG, " estimateCrop=" + estimateCrop); Slog.v(TAG, " sampleSize=" + sampleSize); + Slog.v(TAG, " user defined crops: " + wallpaper.mCropHints); + Slog.v(TAG, " all crops: " + defaultCrops); } Slog.v(TAG, " targetSize=" + safeWidth + "x" + safeHeight); Slog.v(TAG, " maxTextureSize=" + GLHelper.getMaxTextureSize()); diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index 54024e92f95f..02c8a4999f4d 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -20,9 +20,11 @@ import static android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND; import static android.app.ActivityManager.PROCESS_STATE_NONEXISTENT; import static android.app.ActivityOptions.BackgroundActivityStartMode; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_COMPAT; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; @@ -226,6 +228,21 @@ public class BackgroundActivityStartController { }; } + static String balStartModeToString(@BackgroundActivityStartMode int startMode) { + return switch (startMode) { + case MODE_BACKGROUND_ACTIVITY_START_ALLOWED -> "MODE_BACKGROUND_ACTIVITY_START_ALLOWED"; + case MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED -> + "MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED"; + case MODE_BACKGROUND_ACTIVITY_START_COMPAT -> "MODE_BACKGROUND_ACTIVITY_START_COMPAT"; + case MODE_BACKGROUND_ACTIVITY_START_DENIED -> "MODE_BACKGROUND_ACTIVITY_START_DENIED"; + case MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS -> + "MODE_BACKGROUND_ACTIVITY_START_ALWAYS"; + case MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE -> + "MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE"; + default -> "MODE_BACKGROUND_ACTIVITY_START_ALLOWED(" + startMode + ")"; + }; + } + @GuardedBy("mService.mGlobalLock") private final HashMap<Integer, FinishedActivityEntry> mTaskIdToFinishedActivity = new HashMap<>(); @@ -464,10 +481,6 @@ public class BackgroundActivityStartController { this.mResultForRealCaller = resultForRealCaller; } - public boolean isPendingIntentBalAllowedByPermission() { - return PendingIntentRecord.isPendingIntentBalAllowedByPermission(mCheckedOptions); - } - public boolean callerExplicitOptInOrAutoOptIn() { if (mAutoOptInCaller) { return !callerExplicitOptOut(); @@ -528,6 +541,8 @@ public class BackgroundActivityStartController { sb.append("; balAllowedByPiCreatorWithHardening: ") .append(mBalAllowedByPiCreatorWithHardening); sb.append("; resultIfPiCreatorAllowsBal: ").append(mResultForCaller); + sb.append("; callerStartMode: ").append(balStartModeToString( + mCheckedOptions.getPendingIntentBackgroundActivityStartMode())); sb.append("; hasRealCaller: ").append(hasRealCaller()); sb.append("; isCallForResult: ").append(mIsCallForResult); sb.append("; isPendingIntent: ").append(isPendingIntent()); @@ -553,6 +568,8 @@ public class BackgroundActivityStartController { } sb.append("; balAllowedByPiSender: ").append(mBalAllowedByPiSender); sb.append("; resultIfPiSenderAllowsBal: ").append(mResultForRealCaller); + sb.append("; realCallerStartMode: ").append(balStartModeToString( + mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode())); } // features sb.append("; balImproveRealCallerVisibilityCheck: ") @@ -949,7 +966,8 @@ public class BackgroundActivityStartController { } } - if (state.isPendingIntentBalAllowedByPermission() + if (state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode() + == MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS && hasBalPermission(state.mRealCallingUid, state.mRealCallingPid)) { return new BalVerdict(BAL_ALLOW_PERMISSION, /*background*/ false, diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java index 3b999549b302..19941741ed19 100644 --- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java +++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java @@ -52,7 +52,7 @@ import java.util.Objects; * display change transition. In this case, we will queue all display updates until the current * transition's collection finishes and then apply them afterwards. */ -public class DeferredDisplayUpdater implements DisplayUpdater { +class DeferredDisplayUpdater { /** * List of fields that could be deferred before applying to DisplayContent. @@ -110,7 +110,7 @@ public class DeferredDisplayUpdater implements DisplayUpdater { continueScreenUnblocking(); }; - public DeferredDisplayUpdater(@NonNull DisplayContent displayContent) { + DeferredDisplayUpdater(@NonNull DisplayContent displayContent) { mDisplayContent = displayContent; mNonOverrideDisplayInfo.copyFrom(mDisplayContent.getDisplayInfo()); } @@ -122,8 +122,7 @@ public class DeferredDisplayUpdater implements DisplayUpdater { * * @param finishCallback is called when all pending display updates are finished */ - @Override - public void updateDisplayInfo(@NonNull Runnable finishCallback) { + void updateDisplayInfo(@NonNull Runnable finishCallback) { // Get the latest display parameters from the DisplayManager final DisplayInfo displayInfo = getCurrentDisplayInfo(); @@ -310,9 +309,11 @@ public class DeferredDisplayUpdater implements DisplayUpdater { return !Objects.equals(first.uniqueId, second.uniqueId); } - @Override - public void onDisplayContentDisplayPropertiesPostChanged(int previousRotation, int newRotation, - DisplayAreaInfo newDisplayAreaInfo) { + /** + * Called after physical display has changed and after DisplayContent applied new display + * properties. + */ + void onDisplayContentDisplayPropertiesPostChanged() { // Unblock immediately in case there is no transition. This is unlikely to happen. if (mScreenUnblocker != null && !mDisplayContent.mTransitionController.inTransition()) { mScreenUnblocker.sendToTarget(); @@ -320,13 +321,16 @@ public class DeferredDisplayUpdater implements DisplayUpdater { } } - @Override - public void onDisplaySwitching(boolean switching) { + /** + * Called with {@code true} when physical display is going to switch. And {@code false} when + * the display is turned on or the device goes to sleep. + */ + void onDisplaySwitching(boolean switching) { mShouldWaitForTransitionWhenScreenOn = switching; } - @Override - public boolean waitForTransition(@NonNull Message screenUnblocker) { + /** Returns {@code true} if the transition will control when to turn on the screen. */ + boolean waitForTransition(@NonNull Message screenUnblocker) { if (!Flags.waitForTransitionOnDisplaySwitch()) return false; if (!mShouldWaitForTransitionWhenScreenOn) { return false; diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 403c3079e968..b31ae908251e 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -159,7 +159,6 @@ import static com.android.server.wm.utils.DisplayInfoOverrides.WM_OVERRIDE_FIELD import static com.android.server.wm.utils.DisplayInfoOverrides.copyDisplayInfoFields; import static com.android.server.wm.utils.RegionUtils.forEachRectReverse; import static com.android.server.wm.utils.RegionUtils.rectListToRegion; -import static com.android.window.flags.Flags.deferDisplayUpdates; import static com.android.window.flags.Flags.explicitRefreshRateHints; import android.annotation.IntDef; @@ -478,7 +477,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp AppCompatCameraPolicy mAppCompatCameraPolicy; DisplayFrames mDisplayFrames; - final DisplayUpdater mDisplayUpdater; + final DeferredDisplayUpdater mDisplayUpdater; private boolean mInTouchMode; @@ -623,7 +622,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp @VisibleForTesting final DeviceStateController mDeviceStateController; final Consumer<DeviceStateController.DeviceState> mDeviceStateConsumer; - final PhysicalDisplaySwitchTransitionLauncher mDisplaySwitchTransitionLauncher; final RemoteDisplayChangeController mRemoteDisplayChangeController; /** Windows added since {@link #mCurrentFocus} was set to null. Used for ANR blaming. */ @@ -1140,11 +1138,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mWallpaperController.resetLargestDisplay(display); display.getDisplayInfo(mDisplayInfo); display.getMetrics(mDisplayMetrics); - if (deferDisplayUpdates()) { - mDisplayUpdater = new DeferredDisplayUpdater(this); - } else { - mDisplayUpdater = new ImmediateDisplayUpdater(this); - } + mDisplayUpdater = new DeferredDisplayUpdater(this); mSystemGestureExclusionLimit = mWmService.mConstants.mSystemGestureExclusionLimitDp * mDisplayMetrics.densityDpi / DENSITY_DEFAULT; isDefaultDisplay = mDisplayId == DEFAULT_DISPLAY; @@ -1168,8 +1162,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mAppTransitionController = new AppTransitionController(mWmService, this); mTransitionController.registerLegacyListener(mFixedRotationTransitionListener); mUnknownAppVisibilityController = new UnknownAppVisibilityController(mWmService, this); - mDisplaySwitchTransitionLauncher = new PhysicalDisplaySwitchTransitionLauncher(this, - mTransitionController); mRemoteDisplayChangeController = new RemoteDisplayChangeController(this); final InputChannel inputChannel = mWmService.mInputManager.monitorInput( @@ -1190,7 +1182,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mDeviceStateConsumer = (@NonNull DeviceStateController.DeviceState newFoldState) -> { - mDisplaySwitchTransitionLauncher.foldStateChanged(newFoldState); mDisplayRotation.foldStateChanged(newFoldState); }; mDeviceStateController.registerDeviceStateCallback(mDeviceStateConsumer, @@ -3094,8 +3085,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // metrics are updated as rotation settings might depend on them mWmService.mDisplayWindowSettings.applySettingsToDisplayLocked(this, /* includeRotationSettings */ false); - mDisplayUpdater.onDisplayContentDisplayPropertiesPreChanged(mDisplayId, - mInitialDisplayWidth, mInitialDisplayHeight, newWidth, newHeight); mDisplayRotation.physicalDisplayChanged(); mDisplayPolicy.physicalDisplayChanged(); } @@ -3130,8 +3119,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp if (physicalDisplayChanged) { mDisplayPolicy.physicalDisplayUpdated(); - mDisplayUpdater.onDisplayContentDisplayPropertiesPostChanged(currentRotation, - getRotation(), getDisplayAreaInfo()); + mDisplayUpdater.onDisplayContentDisplayPropertiesPostChanged(); } } } diff --git a/services/core/java/com/android/server/wm/DisplayUpdater.java b/services/core/java/com/android/server/wm/DisplayUpdater.java deleted file mode 100644 index 918b180ab1cb..000000000000 --- a/services/core/java/com/android/server/wm/DisplayUpdater.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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.server.wm; - -import android.annotation.NonNull; -import android.os.Message; -import android.view.Surface; -import android.window.DisplayAreaInfo; - -/** - * Interface for a helper class that manages updates of DisplayInfo coming from DisplayManager - */ -interface DisplayUpdater { - /** - * Reads the latest display parameters from the display manager and returns them in a callback. - * If there are pending display updates, it will wait for them to finish first and only then it - * will call the callback with the latest display parameters. - * - * @param callback is called when all pending display updates are finished - */ - void updateDisplayInfo(@NonNull Runnable callback); - - /** - * Called when physical display has changed and before DisplayContent has applied new display - * properties - */ - default void onDisplayContentDisplayPropertiesPreChanged(int displayId, int initialDisplayWidth, - int initialDisplayHeight, int newWidth, int newHeight) { - } - - /** - * Called after physical display has changed and after DisplayContent applied new display - * properties - */ - default void onDisplayContentDisplayPropertiesPostChanged( - @Surface.Rotation int previousRotation, @Surface.Rotation int newRotation, - @NonNull DisplayAreaInfo newDisplayAreaInfo) { - } - - /** - * Called with {@code true} when physical display is going to switch. And {@code false} when - * the display is turned on or the device goes to sleep. - */ - default void onDisplaySwitching(boolean switching) { - } - - /** Returns {@code true} if the transition will control when to turn on the screen. */ - default boolean waitForTransition(@NonNull Message screenUnBlocker) { - return false; - } -} diff --git a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java index e0d69b063573..4ec318bee726 100644 --- a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java +++ b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java @@ -16,9 +16,12 @@ package com.android.server.wm; +import static android.content.pm.ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES; + import android.annotation.NonNull; import android.annotation.Nullable; import android.app.WindowConfiguration; +import android.companion.virtualdevice.flags.Flags; import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; @@ -26,6 +29,7 @@ import android.os.Process; import android.os.UserHandle; import android.util.ArraySet; import android.util.Slog; +import android.view.Display; import android.window.DisplayWindowPolicyController; import java.io.PrintWriter; @@ -80,6 +84,9 @@ class DisplayWindowPolicyControllerHelper { if (hasDisplayCategory(activities.get(i))) { return false; } + if (!launchAllowedByDisplayPolicy(activities.get(i))) { + return false; + } } return true; } @@ -95,7 +102,13 @@ class DisplayWindowPolicyControllerHelper { if (mDisplayWindowPolicyController == null) { // Missing controller means that this display has no categories for activity launch // restriction. - return !hasDisplayCategory(activityInfo); + if (hasDisplayCategory(activityInfo)) { + return false; + } + if (!launchAllowedByDisplayPolicy(activityInfo)) { + return false; + } + return true; } return mDisplayWindowPolicyController.canActivityBeLaunched(activityInfo, intent, windowingMode, launchingFromDisplayId, isNewTask); @@ -112,6 +125,24 @@ class DisplayWindowPolicyControllerHelper { return false; } + private boolean launchAllowedByDisplayPolicy(ActivityInfo aInfo) { + if (!Flags.enforceRemoteDeviceOptOutOnAllVirtualDisplays()) { + return true; + } + int displayType = mDisplayContent.getDisplay().getType(); + if (displayType != Display.TYPE_VIRTUAL && displayType != Display.TYPE_WIFI) { + return true; + } + if ((aInfo.flags & FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES) == 0) { + Slog.d(TAG, + String.format("Checking activity launch on display %d, activity requires" + + " android:canDisplayOnRemoteDevices=true", + mDisplayContent.mDisplayId)); + return false; + } + return true; + } + /** * @see DisplayWindowPolicyController#keepActivityOnWindowFlagsChanged(ActivityInfo, int, int) */ diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java b/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java index 8bd8098b6be9..27e6e0997c89 100644 --- a/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java +++ b/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java @@ -55,8 +55,10 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; /** * Implementation of {@link SettingsProvider} that reads the base settings provided in a display @@ -152,19 +154,35 @@ class DisplayWindowSettingsProvider implements SettingsProvider { /** * Removes display override settings that are no longer associated with active displays. - * This is necessary because displays can be dynamically added or removed during - * the system's lifecycle (e.g., user switch, system server restart). + * <p> + * This cleanup process is essential due to the dynamic nature of displays, which can + * be added or removed during various system events such as user switching or + * system server restarts. * - * @param root The root window container used to obtain the currently active displays. + * @param wms the WindowManagerService instance for retrieving all possible {@link DisplayInfo} + * for the given logical display. + * @param root the root window container used to obtain the currently active displays. */ - void removeStaleDisplaySettings(@NonNull RootWindowContainer root) { + void removeStaleDisplaySettingsLocked(@NonNull WindowManagerService wms, + @NonNull RootWindowContainer root) { if (!Flags.perUserDisplayWindowSettings()) { return; } final Set<String> displayIdentifiers = new ArraySet<>(); + final Consumer<DisplayInfo> addDisplayIdentifier = + displayInfo -> displayIdentifiers.add(mOverrideSettings.getIdentifier(displayInfo)); root.forAllDisplays(dc -> { - final String identifier = mOverrideSettings.getIdentifier(dc.getDisplayInfo()); - displayIdentifiers.add(identifier); + // Begin with the current display's information. Note that the display layout of the + // current device state might not include this display (e.g., external or virtual + // displays), resulting in empty possible display info. + addDisplayIdentifier.accept(dc.getDisplayInfo()); + + // Then, add all possible display information for this display if available. + final List<DisplayInfo> displayInfos = wms.getPossibleDisplayInfoLocked(dc.mDisplayId); + final int size = displayInfos.size(); + for (int i = 0; i < size; i++) { + addDisplayIdentifier.accept(displayInfos.get(i)); + } }); mOverrideSettings.removeStaleDisplaySettings(displayIdentifiers); } diff --git a/services/core/java/com/android/server/wm/ImmediateDisplayUpdater.java b/services/core/java/com/android/server/wm/ImmediateDisplayUpdater.java deleted file mode 100644 index 4af9013d7f4a..000000000000 --- a/services/core/java/com/android/server/wm/ImmediateDisplayUpdater.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.server.wm; - -import android.annotation.NonNull; -import android.view.DisplayInfo; -import android.window.DisplayAreaInfo; - -/** - * DisplayUpdater that immediately applies new DisplayInfo properties - */ -public class ImmediateDisplayUpdater implements DisplayUpdater { - - private final DisplayContent mDisplayContent; - private final DisplayInfo mDisplayInfo = new DisplayInfo(); - - public ImmediateDisplayUpdater(@NonNull DisplayContent displayContent) { - mDisplayContent = displayContent; - mDisplayInfo.copyFrom(mDisplayContent.getDisplayInfo()); - } - - @Override - public void updateDisplayInfo(Runnable callback) { - mDisplayContent.mWmService.mDisplayManagerInternal.getNonOverrideDisplayInfo( - mDisplayContent.mDisplayId, mDisplayInfo); - mDisplayContent.onDisplayInfoUpdated(mDisplayInfo); - callback.run(); - } - - @Override - public void onDisplayContentDisplayPropertiesPreChanged(int displayId, int initialDisplayWidth, - int initialDisplayHeight, int newWidth, int newHeight) { - mDisplayContent.mDisplaySwitchTransitionLauncher.requestDisplaySwitchTransitionIfNeeded( - displayId, initialDisplayWidth, initialDisplayHeight, newWidth, newHeight); - } - - @Override - public void onDisplayContentDisplayPropertiesPostChanged(int previousRotation, int newRotation, - DisplayAreaInfo newDisplayAreaInfo) { - mDisplayContent.mDisplaySwitchTransitionLauncher.onDisplayUpdated(previousRotation, - newRotation, - newDisplayAreaInfo); - } -} diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java index b496a65ba4a6..b8869f159169 100644 --- a/services/core/java/com/android/server/wm/InputMonitor.java +++ b/services/core/java/com/android/server/wm/InputMonitor.java @@ -439,8 +439,7 @@ final class InputMonitor { final InputMethodManagerInternal inputMethodManagerInternal = LocalServices.getService(InputMethodManagerInternal.class); if (inputMethodManagerInternal != null) { - // TODO(b/308479256): Check if hiding "all" IMEs is OK or not. - inputMethodManagerInternal.hideAllInputMethods( + inputMethodManagerInternal.hideInputMethod( SoftInputShowHideReason.HIDE_RECENTS_ANIMATION, mDisplayContent.getDisplayId()); } diff --git a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java b/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java deleted file mode 100644 index 3cf301c1d730..000000000000 --- a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.wm; - -import static android.view.WindowManager.TRANSIT_CHANGE; -import static android.view.WindowManager.TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH; - -import static com.android.internal.R.bool.config_unfoldTransitionEnabled; -import static com.android.server.wm.ActivityTaskManagerService.POWER_MODE_REASON_CHANGE_DISPLAY; -import static com.android.server.wm.DeviceStateController.DeviceState.FOLDED; -import static com.android.server.wm.DeviceStateController.DeviceState.HALF_FOLDED; -import static com.android.server.wm.DeviceStateController.DeviceState.OPEN; - -import android.animation.ValueAnimator; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.content.Context; -import android.graphics.Rect; -import android.window.DisplayAreaInfo; -import android.window.TransitionRequestInfo; -import android.window.WindowContainerTransaction; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.ProtoLogGroup; -import com.android.internal.protolog.ProtoLog; -import com.android.server.wm.DeviceStateController.DeviceState; - -public class PhysicalDisplaySwitchTransitionLauncher { - - private final DisplayContent mDisplayContent; - private final ActivityTaskManagerService mAtmService; - private final Context mContext; - private final TransitionController mTransitionController; - - /** - * If on a foldable device represents whether we need to show unfold animation when receiving - * a physical display switch event - */ - private boolean mShouldRequestTransitionOnDisplaySwitch = false; - /** - * Current device state from {@link android.hardware.devicestate.DeviceStateManager} - */ - private DeviceState mDeviceState = DeviceState.UNKNOWN; - private Transition mTransition; - - public PhysicalDisplaySwitchTransitionLauncher(DisplayContent displayContent, - TransitionController transitionController) { - this(displayContent, displayContent.mWmService.mAtmService, - displayContent.mWmService.mContext, transitionController); - } - - @VisibleForTesting - public PhysicalDisplaySwitchTransitionLauncher(DisplayContent displayContent, - ActivityTaskManagerService service, Context context, - TransitionController transitionController) { - mDisplayContent = displayContent; - mAtmService = service; - mContext = context; - mTransitionController = transitionController; - } - - /** - * Called by the display manager just before it applied the device state, it is guaranteed - * that in case of physical display change the - * {@link PhysicalDisplaySwitchTransitionLauncher#requestDisplaySwitchTransitionIfNeeded} - * method will be invoked *after* this one. - */ - void foldStateChanged(DeviceState newDeviceState) { - boolean isUnfolding = mDeviceState == FOLDED - && (newDeviceState == HALF_FOLDED || newDeviceState == OPEN); - - if (isUnfolding) { - // Request transition only if we are unfolding the device - mShouldRequestTransitionOnDisplaySwitch = true; - } else if (newDeviceState != HALF_FOLDED && newDeviceState != OPEN) { - // Cancel the transition request in case if we are folding or switching to back - // to the rear display before the displays got switched - mShouldRequestTransitionOnDisplaySwitch = false; - } - - mDeviceState = newDeviceState; - } - - /** - * Requests to start a transition for the physical display switch - */ - public void requestDisplaySwitchTransitionIfNeeded(int displayId, int oldDisplayWidth, - int oldDisplayHeight, int newDisplayWidth, int newDisplayHeight) { - if (!mShouldRequestTransitionOnDisplaySwitch) return; - if (!mTransitionController.isShellTransitionsEnabled()) return; - if (!mDisplayContent.getLastHasContent()) return; - - boolean shouldRequestUnfoldTransition = mContext.getResources() - .getBoolean(config_unfoldTransitionEnabled) && ValueAnimator.areAnimatorsEnabled(); - - if (!shouldRequestUnfoldTransition) { - return; - } - - mTransition = null; - - if (mTransitionController.isCollecting()) { - // Add display container to the currently collecting transition - mTransition = mTransitionController.getCollectingTransition(); - mTransition.collect(mDisplayContent); - - // Make sure that transition is not ready until we finish the remote display change - mTransition.setReady(mDisplayContent, false); - mTransition.addFlag(TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH); - - ProtoLog.d(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, - "Adding display switch to existing collecting transition"); - } else { - final TransitionRequestInfo.DisplayChange displayChange = - new TransitionRequestInfo.DisplayChange(displayId); - - final Rect startAbsBounds = new Rect(0, 0, oldDisplayWidth, oldDisplayHeight); - displayChange.setStartAbsBounds(startAbsBounds); - final Rect endAbsBounds = new Rect(0, 0, newDisplayWidth, newDisplayHeight); - displayChange.setEndAbsBounds(endAbsBounds); - displayChange.setPhysicalDisplayChanged(true); - - mTransition = mTransitionController.requestStartDisplayTransition(TRANSIT_CHANGE, - 0 /* flags */, mDisplayContent, null /* remoteTransition */, displayChange); - mTransition.collect(mDisplayContent); - } - - if (mTransition != null) { - mAtmService.startPowerMode(POWER_MODE_REASON_CHANGE_DISPLAY); - } - - mShouldRequestTransitionOnDisplaySwitch = false; - } - - /** - * Called when physical display is getting updated, this could happen e.g. on foldable - * devices when the physical underlying display is replaced. - * - * @param fromRotation rotation before the display change - * @param toRotation rotation after the display change - * @param newDisplayAreaInfo display area info after the display change - */ - public void onDisplayUpdated(int fromRotation, int toRotation, - @NonNull DisplayAreaInfo newDisplayAreaInfo) { - if (mTransition == null) return; - - final boolean started = mDisplayContent.mRemoteDisplayChangeController - .performRemoteDisplayChange(fromRotation, toRotation, newDisplayAreaInfo, - this::continueDisplayUpdate); - - if (!started) { - markTransitionAsReady(); - } - } - - private void continueDisplayUpdate(@Nullable WindowContainerTransaction transaction) { - if (mTransition == null) return; - - if (transaction != null) { - mAtmService.mWindowOrganizerController.applyTransaction(transaction); - } - - markTransitionAsReady(); - } - - private void markTransitionAsReady() { - if (mTransition == null) return; - - mTransition.setAllReady(); - mTransition = null; - } - -} diff --git a/services/core/java/com/android/server/wm/PossibleDisplayInfoMapper.java b/services/core/java/com/android/server/wm/PossibleDisplayInfoMapper.java index e3a2065838d1..6e8797791c97 100644 --- a/services/core/java/com/android/server/wm/PossibleDisplayInfoMapper.java +++ b/services/core/java/com/android/server/wm/PossibleDisplayInfoMapper.java @@ -57,7 +57,7 @@ public class PossibleDisplayInfoMapper { * Returns, for the given displayId, a list of unique display infos. List contains each * supported device state. * <p>List contents are guaranteed to be unique, but returned as a list rather than a set to - * minimize copies needed to make an iteraable data structure. + * minimize copies needed to make an iterable data structure. */ public List<DisplayInfo> getPossibleDisplayInfos(int displayId) { // Update display infos before returning, since any cached values would have been removed diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index c83b28055a2f..ed0dc3be9465 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -2819,7 +2819,21 @@ class TaskFragment extends WindowContainer<WindowContainer> { mClearedTaskForReuse, mClearedTaskFragmentForPip, mClearedForReorderActivityToFront, - calculateMinDimension()); + calculateMinDimension(), + isTopNonFinishingChild()); + } + + private boolean isTopNonFinishingChild() { + final WindowContainer<?> parent = getParent(); + if (parent == null) { + // Either the TaskFragment is not attached or is going to destroy. Return false. + return false; + } + final ActivityRecord topNonFishingActivity = parent.getActivity(ar -> !ar.finishing); + // If the parent's top non-finishing activity is this TaskFragment's, it means + // this TaskFragment is the top non-finishing container of its parent. + return topNonFishingActivity != null && topNonFishingActivity + .equals(getTopNonFinishingActivity()); } /** diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 358adc3352e7..af3ed28d2c2a 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -2185,8 +2185,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { for (int i = mParticipants.size() - 1; i >= 0; --i) { final WallpaperWindowToken wallpaper = mParticipants.valueAt(i).asWallpaperToken(); if (wallpaper != null) { - if (!wallpaper.isVisible() && (wallpaper.isVisibleRequested() - || (Flags.ensureWallpaperInTransitions() && showWallpaper))) { + if (!wallpaper.isVisible() && wallpaper.isVisibleRequested()) { wallpaper.commitVisibility(showWallpaper); } else if (Flags.ensureWallpaperInTransitions() && wallpaper.isVisible() && !showWallpaper && !wallpaper.getDisplayContent().isKeyguardLocked() diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 4db62478f76e..13453a63a1ab 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -118,12 +118,12 @@ import static com.android.server.LockGuard.installLock; import static com.android.server.policy.PhoneWindowManager.TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD; import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER; import static com.android.server.wm.ActivityTaskManagerService.POWER_MODE_REASON_CHANGE_DISPLAY; -import static com.android.server.wm.DisplayContent.IME_TARGET_CONTROL; -import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING; import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND; import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING; import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR; import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER; +import static com.android.server.wm.DisplayContent.IME_TARGET_CONTROL; +import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING; import static com.android.server.wm.RootWindowContainer.MATCH_ATTACHED_TASK_OR_RECENT_TASKS; import static com.android.server.wm.SensitiveContentPackages.PackageInfo; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_ALL; @@ -357,7 +357,6 @@ import com.android.server.policy.WindowManagerPolicy.ScreenOffListener; import com.android.server.power.ShutdownThread; import com.android.server.utils.PriorityDump; import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils; -import com.android.window.flags.Flags; import dalvik.annotation.optimization.NeverCompile; @@ -3750,7 +3749,7 @@ public class WindowManagerService extends IWindowManager.Stub } mCurrentUserId = newUserId; mDisplayWindowSettingsProvider.setOverrideSettingsForUser(newUserId); - mDisplayWindowSettingsProvider.removeStaleDisplaySettings(mRoot); + mDisplayWindowSettingsProvider.removeStaleDisplaySettingsLocked(this, mRoot); mPolicy.setCurrentUserLw(newUserId); mKeyguardDisableHandler.setCurrentUser(newUserId); @@ -5491,7 +5490,7 @@ public class WindowManagerService extends IWindowManager.Stub mRoot.forAllDisplays(DisplayContent::reconfigureDisplayLocked); // Per-user display settings may leave outdated settings after user switches, especially // during reboots starting with the default user without setCurrentUser called. - mDisplayWindowSettingsProvider.removeStaleDisplaySettings(mRoot); + mDisplayWindowSettingsProvider.removeStaleDisplaySettingsLocked(this, mRoot); } } @@ -9390,11 +9389,6 @@ public class WindowManagerService extends IWindowManager.Stub return focusedActivity; } - if (!Flags.embeddedActivityBackNavFlag()) { - // Return if flag is not enabled. - return focusedActivity; - } - if (!focusedActivity.isEmbedded()) { // Return if the focused activity is not embedded. return focusedActivity; diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java index efcc23f35c78..8ae4f9a41efb 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java @@ -41,6 +41,7 @@ import static org.mockito.Mockito.when; import static java.util.Objects.requireNonNull; +import android.annotation.Nullable; import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; @@ -50,6 +51,7 @@ import android.view.inputmethod.ImeTracker; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.android.dx.mockito.inline.extended.ExtendedMockito; +import com.android.internal.annotations.GuardedBy; import com.android.internal.inputmethod.InputBindResult; import com.android.internal.inputmethod.SoftInputShowHideReason; import com.android.internal.inputmethod.StartInputFlags; @@ -75,7 +77,7 @@ public class DefaultImeVisibilityApplierTest extends InputMethodManagerServiceTe super.setUp(); synchronized (ImfLock.class) { mVisibilityApplier = mInputMethodManagerService.getVisibilityApplierLocked(); - mInputMethodManagerService.setAttachedClientForTesting(requireNonNull( + setAttachedClientLocked(requireNonNull( mInputMethodManagerService.getClientStateLocked(mMockInputMethodClient))); } } @@ -168,7 +170,9 @@ public class DefaultImeVisibilityApplierTest extends InputMethodManagerServiceTe // Init a IME target client on the secondary display to show IME. mInputMethodManagerService.addClient(mMockInputMethodClient, mMockRemoteInputConnection, 10 /* selfReportedDisplayId */); - mInputMethodManagerService.setAttachedClientForTesting(null); + synchronized (ImfLock.class) { + setAttachedClientLocked(null); + } startInputOrWindowGainedFocus(mWindowToken, SOFT_INPUT_STATE_ALWAYS_VISIBLE); final var statsToken = ImeTracker.Token.empty(); @@ -206,7 +210,9 @@ public class DefaultImeVisibilityApplierTest extends InputMethodManagerServiceTe @Test public void testApplyImeVisibility_hideImeWhenUnbinding() { - mInputMethodManagerService.setAttachedClientForTesting(null); + synchronized (ImfLock.class) { + setAttachedClientLocked(null); + } startInputOrWindowGainedFocus(mWindowToken, SOFT_INPUT_STATE_ALWAYS_VISIBLE); ExtendedMockito.spyOn(mVisibilityApplier); @@ -233,6 +239,11 @@ public class DefaultImeVisibilityApplierTest extends InputMethodManagerServiceTe } } + @GuardedBy("ImfLock.class") + private void setAttachedClientLocked(@Nullable ClientState cs) { + mInputMethodManagerService.getUserData(mUserId).mCurClient = cs; + } + private InputBindResult startInputOrWindowGainedFocus(IBinder windowToken, int softInputMode) { return mInputMethodManagerService.startInputOrWindowGainedFocus( StartInputReason.WINDOW_FOCUS_GAIN /* startInputReason */, diff --git a/services/tests/displayservicetests/src/com/android/server/display/BrightnessMappingStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/BrightnessMappingStrategyTest.java index fb73aff44c64..f3cd0c960fca 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/BrightnessMappingStrategyTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/BrightnessMappingStrategyTest.java @@ -693,6 +693,21 @@ public class BrightnessMappingStrategyTest { } @Test + public void testGetPreset() { + int preset = Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_DIM; + Settings.System.putInt(mContext.getContentResolver(), + Settings.System.SCREEN_BRIGHTNESS_FOR_ALS, preset); + setUpResources(); + DisplayDeviceConfig ddc = new DdcBuilder() + .setAutoBrightnessLevels(AUTO_BRIGHTNESS_MODE_DEFAULT, preset, DISPLAY_LEVELS) + .setAutoBrightnessLevelsLux(AUTO_BRIGHTNESS_MODE_DEFAULT, preset, LUX_LEVELS) + .build(); + BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(mContext, ddc, + AUTO_BRIGHTNESS_MODE_DEFAULT, /* displayWhiteBalanceController= */ null); + assertEquals(preset, strategy.getPreset()); + } + + @Test public void testAutoBrightnessModeAndPreset() { int mode = AUTO_BRIGHTNESS_MODE_DOZE; int preset = Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_DIM; diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt index d672435096b9..6929690baaf8 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt @@ -225,5 +225,37 @@ class BrightnessLowLuxModifierTest { assertThat(modifier.brightnessReason).isEqualTo(BrightnessReason.MODIFIER_MIN_LUX) assertThat(modifier.brightnessLowerBound).isEqualTo(LOW_LUX_BRIGHTNESS) } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER) + fun testUserSwitch() { + // nits: 0.5 -> backlight 0.01 -> brightness -> 0.05 + whenever(mockDisplayDeviceConfig.getBacklightFromNits(/* nits= */ 0.5f)) + .thenReturn(0.01f) + whenever(mockDisplayDeviceConfig.getBrightnessFromBacklight(/* backlight = */ 0.01f)) + .thenReturn(0.05f) + + Settings.Secure.putIntForUser(context.contentResolver, + Settings.Secure.EVEN_DIMMER_ACTIVATED, 0, USER_ID) // off + Settings.Secure.putFloatForUser(context.contentResolver, + Settings.Secure.EVEN_DIMMER_MIN_NITS, 1.0f, USER_ID) + + modifier.recalculateLowerBound() + + assertThat(modifier.isActive).isFalse() + assertThat(modifier.brightnessLowerBound).isEqualTo(TRANSITION_POINT) + assertThat(modifier.brightnessReason).isEqualTo(0) // no reason - i.e. off + + Settings.Secure.putIntForUser(context.contentResolver, + Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, USER_ID) // on + Settings.Secure.putFloatForUser(context.contentResolver, + Settings.Secure.EVEN_DIMMER_MIN_NITS, 0.5f, USER_ID) + modifier.onSwitchUser() + + assertThat(modifier.isActive).isTrue() + assertThat(modifier.brightnessReason).isEqualTo( + BrightnessReason.MODIFIER_MIN_USER_SET_LOWER_BOUND) + assertThat(modifier.brightnessLowerBound).isEqualTo(0.05f) + } } diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/LightSensorControllerTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/LightSensorControllerTest.kt index f59e1275d2ce..79b99b5294d7 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/LightSensorControllerTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/LightSensorControllerTest.kt @@ -29,6 +29,7 @@ import com.android.server.display.config.SensorData import com.android.server.display.config.createSensorData import com.android.server.display.utils.AmbientFilter import org.junit.Before +import org.junit.Test import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq @@ -62,31 +63,35 @@ class LightSensorControllerTest { mockLightSensorListener, mockHandler, testInjector) } - fun `does not register light sensor if is not configured`() { + @Test + fun doesNotRegisterLightSensorIfNotConfigured() { controller.restart() verifyNoMoreInteractions(mockSensorManager, mockAmbientFilter, mockLightSensorListener) } - fun `does not register light sensor if missing`() { + @Test + fun doesNotRegisterLightSensorIfMissing() { controller.configure(dummySensorData, DISPLAY_ID) controller.restart() verifyNoMoreInteractions(mockSensorManager, mockAmbientFilter, mockLightSensorListener) } - fun `register light sensor if configured and present`() { + @Test + fun registerLightSensorIfConfiguredAndPresent() { testInjector.lightSensor = TestUtils .createSensor(Sensor.TYPE_LIGHT, Sensor.STRING_TYPE_LIGHT) controller.configure(dummySensorData, DISPLAY_ID) controller.restart() verify(mockSensorManager).registerListener(any(), - testInjector.lightSensor, LIGHT_SENSOR_RATE * 1000, mockHandler) + eq(testInjector.lightSensor), eq(LIGHT_SENSOR_RATE * 1000), eq(mockHandler)) verifyNoMoreInteractions(mockSensorManager, mockAmbientFilter, mockLightSensorListener) } - fun `register light sensor once if not changed`() { + @Test + fun registerLightSensorOnceIfNotChanged() { testInjector.lightSensor = TestUtils .createSensor(Sensor.TYPE_LIGHT, Sensor.STRING_TYPE_LIGHT) controller.configure(dummySensorData, DISPLAY_ID) @@ -95,11 +100,12 @@ class LightSensorControllerTest { controller.restart() verify(mockSensorManager).registerListener(any(), - testInjector.lightSensor, LIGHT_SENSOR_RATE * 1000, mockHandler) + eq(testInjector.lightSensor), eq(LIGHT_SENSOR_RATE * 1000), eq(mockHandler)) verifyNoMoreInteractions(mockSensorManager, mockAmbientFilter, mockLightSensorListener) } - fun `register new light sensor and unregister old if changed`() { + @Test + fun registerNewAndUnregisterOldLightSensorIfChanged() { val lightSensor1 = TestUtils .createSensor(Sensor.TYPE_LIGHT, Sensor.STRING_TYPE_LIGHT) testInjector.lightSensor = lightSensor1 @@ -112,19 +118,21 @@ class LightSensorControllerTest { controller.configure(dummySensorData, DISPLAY_ID) controller.restart() - inOrder { + inOrder(mockSensorManager, mockAmbientFilter, mockLightSensorListener) { verify(mockSensorManager).registerListener(any(), - lightSensor1, LIGHT_SENSOR_RATE * 1000, mockHandler) - verify(mockSensorManager).unregisterListener(any<SensorEventListener>()) + eq(lightSensor1), eq(LIGHT_SENSOR_RATE * 1000), eq(mockHandler)) + verify(mockSensorManager).registerListener(any(), + eq(lightSensor2), eq(LIGHT_SENSOR_RATE * 1000), eq(mockHandler)) + verify(mockSensorManager).unregisterListener(any<SensorEventListener>(), + eq(lightSensor1)) verify(mockAmbientFilter).clear() verify(mockLightSensorListener).onAmbientLuxChange(LightSensorController.INVALID_LUX) - verify(mockSensorManager).registerListener(any(), - lightSensor2, LIGHT_SENSOR_RATE * 1000, mockHandler) } verifyNoMoreInteractions(mockSensorManager, mockAmbientFilter, mockLightSensorListener) } - fun `notifies listener on ambient lux change`() { + @Test + fun notifiesListenerOnAmbientLuxChange() { val expectedLux = 40f val eventLux = 50 val eventTime = 60L @@ -141,7 +149,7 @@ class LightSensorControllerTest { listener.onSensorChanged(TestUtils.createSensorEvent(testInjector.lightSensor, eventLux, eventTime * 1_000_000)) - inOrder { + inOrder(mockAmbientFilter, mockLightSensorListener) { verify(mockAmbientFilter).addValue(eventTime, eventLux.toFloat()) verify(mockLightSensorListener).onAmbientLuxChange(expectedLux) } diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java index 242d5593c3c8..62400ebed149 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java @@ -1088,6 +1088,21 @@ public class DisplayModeDirectorTest { } @Test + public void testModeSwitching_UserSwitch() { + DisplayModeDirector director = createDirectorFromFpsRange(0, 90); + assertThat(director.getModeSwitchingType()).isEqualTo( + DisplayManager.SWITCHING_TYPE_WITHIN_GROUPS); + + int newModeSwitchingType = DisplayManager.SWITCHING_TYPE_NONE; + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.MATCH_CONTENT_FRAME_RATE, newModeSwitchingType); + director.onSwitchUser(); + waitForIdleSync(); + + assertThat(director.getModeSwitchingType()).isEqualTo(newModeSwitchingType); + } + + @Test public void testDefaultDisplayModeIsSelectedIfAvailable() { final float[] refreshRates = new float[]{24f, 25f, 30f, 60f, 90f}; final int defaultModeId = 3; @@ -1883,6 +1898,62 @@ public class DisplayModeDirectorTest { } @Test + public void testPeakRefreshRate_UserSwitch() { + when(mDisplayManagerFlags.isBackUpSmoothDisplayAndForcePeakRefreshRateEnabled()) + .thenReturn(true); + DisplayModeDirector director = + new DisplayModeDirector(mContext, mHandler, mInjector, + mDisplayManagerFlags, mDisplayDeviceConfigProvider); + director.getBrightnessObserver().setDefaultDisplayState(Display.STATE_ON); + + Display.Mode[] modes1 = new Display.Mode[] { + new Display.Mode(/* modeId= */ 1, /* width= */ 1280, /* height= */ 720, + /* refreshRate= */ 60), + new Display.Mode(/* modeId= */ 2, /* width= */ 1280, /* height= */ 720, + /* refreshRate= */ 130), + }; + Display.Mode[] modes2 = new Display.Mode[] { + new Display.Mode(/* modeId= */ 1, /* width= */ 1280, /* height= */ 720, + /* refreshRate= */ 60), + new Display.Mode(/* modeId= */ 2, /* width= */ 1280, /* height= */ 720, + /* refreshRate= */ 140), + }; + SparseArray<Display.Mode[]> supportedModesByDisplay = new SparseArray<>(); + supportedModesByDisplay.put(DISPLAY_ID, modes1); + supportedModesByDisplay.put(DISPLAY_ID_2, modes2); + + Sensor lightSensor = createLightSensor(); + SensorManager sensorManager = createMockSensorManager(lightSensor); + director.start(sensorManager); + director.injectSupportedModesByDisplay(supportedModesByDisplay); + + // Disable Smooth Display + setPeakRefreshRate(RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE); + + Vote vote1 = director.getVote(DISPLAY_ID, + Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE); + Vote vote2 = director.getVote(DISPLAY_ID_2, + Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE); + assertVoteForRenderFrameRateRange(vote1, /* frameRateLow= */ 0, + /* frameRateHigh= */ RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE); + assertVoteForRenderFrameRateRange(vote2, /* frameRateLow= */ 0, + /* frameRateHigh= */ RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE); + + // Switch user to one that has Smooth Display Enabled + Settings.System.putFloat(mContext.getContentResolver(), Settings.System.PEAK_REFRESH_RATE, + Float.POSITIVE_INFINITY); + director.onSwitchUser(); + waitForIdleSync(); + + vote1 = director.getVote(DISPLAY_ID, + Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE); + vote2 = director.getVote(DISPLAY_ID_2, + Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE); + assertVoteForRenderFrameRateRange(vote1, /* frameRateLow= */ 0, /* frameRateHigh= */ 130); + assertVoteForRenderFrameRateRange(vote2, /* frameRateLow= */ 0, /* frameRateHigh= */ 140); + } + + @Test @Parameters({ "true, true, 60", "false, true, 50", @@ -2036,6 +2107,62 @@ public class DisplayModeDirectorTest { } @Test + public void testMinRefreshRate_UserSwitch() { + when(mDisplayManagerFlags.isBackUpSmoothDisplayAndForcePeakRefreshRateEnabled()) + .thenReturn(true); + DisplayModeDirector director = + new DisplayModeDirector(mContext, mHandler, mInjector, + mDisplayManagerFlags, mDisplayDeviceConfigProvider); + director.getBrightnessObserver().setDefaultDisplayState(Display.STATE_ON); + + Display.Mode[] modes1 = new Display.Mode[] { + new Display.Mode(/* modeId= */ 1, /* width= */ 1280, /* height= */ 720, + /* refreshRate= */ 60), + new Display.Mode(/* modeId= */ 2, /* width= */ 1280, /* height= */ 720, + /* refreshRate= */ 130), + }; + Display.Mode[] modes2 = new Display.Mode[] { + new Display.Mode(/* modeId= */ 1, /* width= */ 1280, /* height= */ 720, + /* refreshRate= */ 60), + new Display.Mode(/* modeId= */ 2, /* width= */ 1280, /* height= */ 720, + /* refreshRate= */ 140), + }; + SparseArray<Display.Mode[]> supportedModesByDisplay = new SparseArray<>(); + supportedModesByDisplay.put(DISPLAY_ID, modes1); + supportedModesByDisplay.put(DISPLAY_ID_2, modes2); + + Sensor lightSensor = createLightSensor(); + SensorManager sensorManager = createMockSensorManager(lightSensor); + director.start(sensorManager); + director.injectSupportedModesByDisplay(supportedModesByDisplay); + + // Disable Force Peak Refresh Rate + setMinRefreshRate(0); + + Vote vote1 = director.getVote(DISPLAY_ID, Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE); + Vote vote2 = director.getVote(DISPLAY_ID_2, + Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE); + assertVoteForRenderFrameRateRange(vote1, /* frameRateLow= */ 0, + /* frameRateHigh= */ Float.POSITIVE_INFINITY); + assertVoteForRenderFrameRateRange(vote2, /* frameRateLow= */ 0, + /* frameRateHigh= */ Float.POSITIVE_INFINITY); + + // Switch user to one that has Force Peak Refresh Rate enabled + Settings.System.putFloat(mContext.getContentResolver(), Settings.System.MIN_REFRESH_RATE, + Float.POSITIVE_INFINITY); + director.onSwitchUser(); + waitForIdleSync(); + + vote1 = director.getVote(DISPLAY_ID, Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE); + vote2 = director.getVote(DISPLAY_ID_2, + Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE); + assertVoteForRenderFrameRateRange(vote1, /* frameRateLow= */ 130, + /* frameRateHigh= */ Float.POSITIVE_INFINITY); + assertVoteForRenderFrameRateRange(vote2, /* frameRateLow= */ 140, + /* frameRateHigh= */ Float.POSITIVE_INFINITY); + } + + @Test public void testPeakAndMinRefreshRate_FlagEnabled_DisplayWithOneMode() { when(mDisplayManagerFlags.isBackUpSmoothDisplayAndForcePeakRefreshRateEnabled()) .thenReturn(true); diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java index 8d0b2797d200..fc28f9ef2a13 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java @@ -266,8 +266,8 @@ public class PackageArchiverTest { rule.mocks().getHandler().flush(); ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class); - verify(mIntentSender).sendIntent(any(), anyInt(), intentCaptor.capture(), any(), any(), - any(), any()); + verify(mIntentSender).sendIntent(any(), anyInt(), intentCaptor.capture(), any(), + (Bundle) any(), any(), any()); Intent value = intentCaptor.getValue(); assertThat(value.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)).isEqualTo(PACKAGE); assertThat(value.getIntExtra(PackageInstaller.EXTRA_STATUS, 0)).isEqualTo( @@ -336,8 +336,8 @@ public class PackageArchiverTest { rule.mocks().getHandler().flush(); ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class); - verify(mIntentSender).sendIntent(any(), anyInt(), intentCaptor.capture(), any(), any(), - any(), any()); + verify(mIntentSender).sendIntent(any(), anyInt(), intentCaptor.capture(), any(), + (Bundle) any(), any(), any()); Intent value = intentCaptor.getValue(); assertThat(value.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)).isEqualTo(PACKAGE); assertThat(value.getIntExtra(PackageInstaller.EXTRA_STATUS, 0)).isEqualTo( diff --git a/services/tests/performancehinttests/TEST_MAPPING b/services/tests/performancehinttests/TEST_MAPPING index faffe358f259..fa7b89700952 100644 --- a/services/tests/performancehinttests/TEST_MAPPING +++ b/services/tests/performancehinttests/TEST_MAPPING @@ -1,18 +1,18 @@ { - "ravenwood-postsubmit": [ + "presubmit": [ { - "name": "PerformanceHintTestsRavenwood", - "host": true, + "name": "PerformanceHintTests", "options": [ - {"exclude-annotation": "android.platform.test.annotations.DisabledOnRavenwood"} + {"exclude-annotation": "org.junit.Ignore"} ] } ], - "postsubmit": [ + "ravenwood-postsubmit": [ { - "name": "PerformanceHintTests", + "name": "PerformanceHintTestsRavenwood", + "host": true, "options": [ - {"exclude-annotation": "org.junit.Ignore"} + {"exclude-annotation": "android.platform.test.annotations.DisabledOnRavenwood"} ] } ] diff --git a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java index b737e0f98317..40c521a1dc64 100644 --- a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java +++ b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java @@ -1304,6 +1304,7 @@ public class PowerManagerServiceTest { .setDozeOverrideFromDreamManager( Display.STATE_ON, Display.STATE_REASON_DEFAULT_POLICY, + PowerManager.BRIGHTNESS_INVALID_FLOAT, PowerManager.BRIGHTNESS_DEFAULT); assertTrue(isAcquired[0]); } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java index 20b9592cb9f2..1afe12fc6927 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -840,6 +840,10 @@ public class AccessibilityManagerServiceTest { info_a.setComponentName(COMPONENT_NAME); final AccessibilityServiceInfo info_b = new AccessibilityServiceInfo(); info_b.setComponentName(new ComponentName("package", "class")); + writeStringsToSetting(Set.of( + info_a.getComponentName().flattenToString(), + info_b.getComponentName().flattenToString()), + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); AccessibilityUserState userState = mA11yms.getCurrentUserState(); userState.mInstalledServices.clear(); @@ -858,10 +862,9 @@ public class AccessibilityManagerServiceTest { userState = mA11yms.getCurrentUserState(); assertThat(userState.mEnabledServices).containsExactly(info_b.getComponentName()); //Assert setting change - final Set<ComponentName> componentsFromSetting = new ArraySet<>(); - mA11yms.readComponentNamesFromSettingLocked(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, - userState.mUserId, componentsFromSetting); - assertThat(componentsFromSetting).containsExactly(info_b.getComponentName()); + final Set<String> enabledServices = + readStringsFromSetting(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); + assertThat(enabledServices).containsExactly(info_b.getComponentName().flattenToString()); } @Test @@ -880,6 +883,10 @@ public class AccessibilityManagerServiceTest { info_a.getComponentName().flattenToString(), info_b.getComponentName().flattenToString()), SOFTWARE); + writeStringsToSetting(Set.of( + info_a.getComponentName().flattenToString(), + info_b.getComponentName().flattenToString()), + ShortcutUtils.convertToKey(SOFTWARE)); // despite force stopping both packages, only the first service has the relevant flag, // so only the first should be removed. @@ -896,13 +903,53 @@ public class AccessibilityManagerServiceTest { assertThat(userState.getShortcutTargetsLocked(SOFTWARE)).containsExactly( info_b.getComponentName().flattenToString()); //Assert setting change - final Set<String> targetsFromSetting = new ArraySet<>(); - mA11yms.readColonDelimitedSettingToSet(ShortcutUtils.convertToKey(SOFTWARE), - userState.mUserId, str -> str, targetsFromSetting); + final Set<String> targetsFromSetting = readStringsFromSetting( + ShortcutUtils.convertToKey(SOFTWARE)); assertThat(targetsFromSetting).containsExactly(info_b.getComponentName().flattenToString()); } @Test + public void testPackagesForceStopped_otherServiceStopped_doesNotRemoveContinuousTarget() { + final AccessibilityServiceInfo info_a = new AccessibilityServiceInfo(); + info_a.setComponentName(COMPONENT_NAME); + info_a.flags = FLAG_REQUEST_ACCESSIBILITY_BUTTON; + final AccessibilityServiceInfo info_b = new AccessibilityServiceInfo(); + info_b.setComponentName(new ComponentName("package", "class")); + writeStringsToSetting(Set.of( + info_a.getComponentName().flattenToString(), + info_b.getComponentName().flattenToString()), + ShortcutUtils.convertToKey(SOFTWARE)); + + AccessibilityUserState userState = mA11yms.getCurrentUserState(); + userState.mInstalledServices.clear(); + userState.mInstalledServices.add(info_a); + userState.mInstalledServices.add(info_b); + userState.updateShortcutTargetsLocked(Set.of( + info_a.getComponentName().flattenToString(), + info_b.getComponentName().flattenToString()), + SOFTWARE); + + // Force stopping a service should not disable unrelated continuous services. + synchronized (mA11yms.getLock()) { + mA11yms.onPackagesForceStoppedLocked( + new String[]{info_b.getComponentName().getPackageName()}, + userState); + } + + //Assert user state change + userState = mA11yms.getCurrentUserState(); + assertThat(userState.getShortcutTargetsLocked(SOFTWARE)).containsExactly( + info_a.getComponentName().flattenToString(), + info_b.getComponentName().flattenToString()); + //Assert setting unchanged + final Set<String> targetsFromSetting = readStringsFromSetting( + ShortcutUtils.convertToKey(SOFTWARE)); + assertThat(targetsFromSetting).containsExactly( + info_a.getComponentName().flattenToString(), + info_b.getComponentName().flattenToString()); + } + + @Test public void testPackageMonitorScanPackages_scansWithoutHoldingLock() { setupAccessibilityServiceConnection(0); final AtomicReference<Set<Boolean>> lockState = collectLockStateWhilePackageScanning(); @@ -1844,6 +1891,11 @@ public class AccessibilityManagerServiceTest { return result; } + private void writeStringsToSetting(Set<String> strings, String setting) { + mA11yms.persistColonDelimitedSetToSettingLocked( + setting, UserHandle.USER_SYSTEM, strings, str -> str); + } + private void broadcastSettingRestored(String setting, String newValue) { Intent intent = new Intent(Intent.ACTION_SETTING_RESTORED) .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY) diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java index 3ef81fde6506..60bcecc2f885 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java @@ -620,7 +620,7 @@ public class FullScreenMagnificationGestureHandlerTest { @Test @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_MULTIPLE_FINGER_MULTIPLE_TAP_GESTURE) - public void testTwoFingerTap_StateIsActivated_shouldInDelegating() { + public void testTwoFingerTap_StateIsActivated_shouldInDetecting() { assumeTrue(isWatch()); enableOneFingerPanning(false); goFromStateIdleTo(STATE_ACTIVATED); @@ -629,14 +629,15 @@ public class FullScreenMagnificationGestureHandlerTest { send(downEvent()); send(pointerEvent(ACTION_POINTER_DOWN, DEFAULT_X * 2, DEFAULT_Y)); send(upEvent()); - fastForward(ViewConfiguration.getDoubleTapTimeout()); + fastForward(mMgh.mDetectingState.mMultiTapMaxDelay); - assertTrue(mMgh.mCurrentState == mMgh.mDelegatingState); + verify(mMgh.getNext(), times(3)).onMotionEvent(any(), any(), anyInt()); + assertTrue(mMgh.mCurrentState == mMgh.mDetectingState); } @Test @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_MULTIPLE_FINGER_MULTIPLE_TAP_GESTURE) - public void testTwoFingerTap_StateIsIdle_shouldInDelegating() { + public void testTwoFingerTap_StateIsIdle_shouldInDetecting() { assumeTrue(isWatch()); enableOneFingerPanning(false); goFromStateIdleTo(STATE_IDLE); @@ -645,9 +646,10 @@ public class FullScreenMagnificationGestureHandlerTest { send(downEvent()); send(pointerEvent(ACTION_POINTER_DOWN, DEFAULT_X * 2, DEFAULT_Y)); send(upEvent()); - fastForward(ViewConfiguration.getDoubleTapTimeout()); + fastForward(mMgh.mDetectingState.mMultiTapMaxDelay); - assertTrue(mMgh.mCurrentState == mMgh.mDelegatingState); + verify(mMgh.getNext(), times(3)).onMotionEvent(any(), any(), anyInt()); + assertTrue(mMgh.mCurrentState == mMgh.mDetectingState); } @Test @@ -982,6 +984,53 @@ public class FullScreenMagnificationGestureHandlerTest { } @Test + public void testSingleFingerOverscrollAtTopEdge_isWatch_scrollDiagonally_noOverscroll() { + assumeTrue(isWatch()); + goFromStateIdleTo(STATE_SINGLE_PANNING); + float centerX = + (INITIAL_MAGNIFICATION_BOUNDS.right + INITIAL_MAGNIFICATION_BOUNDS.left) / 2.0f; + mFullScreenMagnificationController.setCenter( + DISPLAY_0, centerX, INITIAL_MAGNIFICATION_BOUNDS.top, false, 1); + final float swipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop() + 1; + PointF initCoords = + new PointF( + mFullScreenMagnificationController.getCenterX(DISPLAY_0), + mFullScreenMagnificationController.getCenterY(DISPLAY_0)); + PointF edgeCoords = new PointF(initCoords.x, initCoords.y); + // Scroll diagonally towards top-right with a bigger right delta + edgeCoords.offset(swipeMinDistance * 2, swipeMinDistance); + + swipeAndHold(initCoords, edgeCoords); + + assertTrue(mMgh.mOverscrollHandler.mOverscrollState == mMgh.OVERSCROLL_NONE); + assertTrue(isZoomed()); + } + + @Test + public void + testSingleFingerOverscrollAtTopEdge_isWatch_scrollDiagonally_expectedOverscrollState() { + assumeTrue(isWatch()); + goFromStateIdleTo(STATE_SINGLE_PANNING); + float centerX = + (INITIAL_MAGNIFICATION_BOUNDS.right + INITIAL_MAGNIFICATION_BOUNDS.left) / 2.0f; + mFullScreenMagnificationController.setCenter( + DISPLAY_0, centerX, INITIAL_MAGNIFICATION_BOUNDS.top, false, 1); + final float swipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop() + 1; + PointF initCoords = + new PointF( + mFullScreenMagnificationController.getCenterX(DISPLAY_0), + mFullScreenMagnificationController.getCenterY(DISPLAY_0)); + PointF edgeCoords = new PointF(initCoords.x, initCoords.y); + // Scroll diagonally towards top-right with a bigger top delta + edgeCoords.offset(swipeMinDistance, swipeMinDistance * 2); + + swipeAndHold(initCoords, edgeCoords); + + assertTrue(mMgh.mOverscrollHandler.mOverscrollState == mMgh.OVERSCROLL_VERTICAL_EDGE); + assertTrue(isZoomed()); + } + + @Test public void testSingleFingerScrollAtEdge_isWatch_noOverscroll() { assumeTrue(isWatch()); goFromStateIdleTo(STATE_SINGLE_PANNING); @@ -1057,9 +1106,24 @@ public class FullScreenMagnificationGestureHandlerTest { assumeTrue(isWatch()); goFromStateIdleTo(STATE_ACTIVATED); - swipeAndHold(); + PointF pointer = DEFAULT_POINT; + send(downEvent(pointer.x, pointer.y)); + + // first move triggers the panning state + pointer.offset(100, 100); + fastForward(20); + send(moveEvent(pointer.x, pointer.y)); + + // second move actually pans + pointer.offset(100, 100); + fastForward(20); + send(moveEvent(pointer.x, pointer.y)); + pointer.offset(100, 100); + fastForward(20); + send(moveEvent(pointer.x, pointer.y)); + fastForward(20); - swipe(DEFAULT_POINT, new PointF(DEFAULT_X * 2, DEFAULT_Y * 2), /* durationMs= */ 20); + send(upEvent(pointer.x, pointer.y)); verify(mMockScroller).fling( /* startX= */ anyInt(), diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java new file mode 100644 index 000000000000..b5a538fa09f8 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java @@ -0,0 +1,144 @@ +/* + * 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.audio; + +import static com.android.media.audio.Flags.asDeviceConnectionFailure; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.Manifest; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.media.AudioDeviceAttributes; +import android.media.AudioManager; +import android.media.AudioSystem; +import android.platform.test.annotations.Presubmit; +import android.util.Log; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.MediumTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; + +@MediumTest +@Presubmit +@RunWith(AndroidJUnit4.class) +public class AudioDeviceInventoryTest { + + private static final String TAG = "AudioDeviceInventoryTest"; + + @Mock private AudioService mMockAudioService; + private AudioDeviceInventory mDevInventory; + @Spy private AudioDeviceBroker mSpyAudioDeviceBroker; + @Spy private AudioSystemAdapter mSpyAudioSystem; + + private SystemServerAdapter mSystemServer; + + private BluetoothDevice mFakeBtDevice; + + @Before + public void setUp() throws Exception { + Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + mMockAudioService = mock(AudioService.class); + mSpyAudioSystem = spy(new NoOpAudioSystemAdapter()); + mDevInventory = new AudioDeviceInventory(mSpyAudioSystem); + mSystemServer = new NoOpSystemServerAdapter(); + mSpyAudioDeviceBroker = spy(new AudioDeviceBroker(context, mMockAudioService, mDevInventory, + mSystemServer, mSpyAudioSystem)); + mDevInventory.setDeviceBroker(mSpyAudioDeviceBroker); + + BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + mFakeBtDevice = adapter.getRemoteDevice("00:01:02:03:04:05"); + } + + @After + public void tearDown() throws Exception { } + + /** + * test that for DEVICE_OUT_BLUETOOTH_A2DP devices, when the device connects, it's only + * added to the connected devices when the connection through AudioSystem is successful + * @throws Exception on error + */ + @Test + public void testSetDeviceConnectionStateA2dp() throws Exception { + Log.i(TAG, "starting testSetDeviceConnectionStateA2dp"); + assertTrue("collection of connected devices not empty at start", + mDevInventory.getConnectedDevices().isEmpty()); + + final AudioDeviceAttributes ada = new AudioDeviceAttributes( + AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, mFakeBtDevice.getAddress()); + AudioDeviceBroker.BtDeviceInfo btInfo = + new AudioDeviceBroker.BtDeviceInfo(mFakeBtDevice, BluetoothProfile.A2DP, + BluetoothProfile.STATE_CONNECTED, AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, + AudioSystem.AUDIO_FORMAT_SBC); + + // test that no device is added when AudioSystem returns AUDIO_STATUS_ERROR + // when setDeviceConnectionState is called for the connection + // NOTE: for now this is only when flag asDeviceConnectionFailure is true + if (asDeviceConnectionFailure()) { + when(mSpyAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE, + AudioSystem.AUDIO_FORMAT_DEFAULT)) + .thenReturn(AudioSystem.AUDIO_STATUS_ERROR); + runWithBluetoothPrivilegedPermission( + () -> mDevInventory.onSetBtActiveDevice(/*btInfo*/ btInfo, + /*codec*/ AudioSystem.AUDIO_FORMAT_DEFAULT, AudioManager.STREAM_MUSIC)); + + assertEquals(0, mDevInventory.getConnectedDevices().size()); + } + + // test that the device is added when AudioSystem returns AUDIO_STATUS_OK + // when setDeviceConnectionState is called for the connection + when(mSpyAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE, + AudioSystem.AUDIO_FORMAT_DEFAULT)) + .thenReturn(AudioSystem.AUDIO_STATUS_OK); + runWithBluetoothPrivilegedPermission( + () -> mDevInventory.onSetBtActiveDevice(/*btInfo*/ btInfo, + /*codec*/ AudioSystem.AUDIO_FORMAT_DEFAULT, AudioManager.STREAM_MUSIC)); + assertEquals(1, mDevInventory.getConnectedDevices().size()); + } + + // TODO add test for hearing aid + + // TODO add test for BLE + + /** + * Executes a Runnable while holding the BLUETOOTH_PRIVILEGED permission + * @param toRunWithPermission the runnable to run with BT privileges + */ + private void runWithBluetoothPrivilegedPermission(Runnable toRunWithPermission) { + try { + InstrumentationRegistry.getInstrumentation().getUiAutomation() + .adoptShellPermissionIdentity(Manifest.permission.BLUETOOTH_PRIVILEGED); + toRunWithPermission.run(); + } finally { + InstrumentationRegistry.getInstrumentation().getUiAutomation() + .dropShellPermissionIdentity(); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java index 37895315557a..36a7b3dff28d 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java @@ -1296,6 +1296,11 @@ public class BiometricSchedulerTest { mFingerprints.add((Fingerprint) identifier); } + @Override + protected int getModality() { + return 0; + } + public List<Fingerprint> getFingerprints() { return mFingerprints; } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClientTest.java index c9482ceb00f5..a34e7965ccee 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClientTest.java @@ -19,6 +19,7 @@ package com.android.server.biometrics.sensors.fingerprint.aidl; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -30,12 +31,16 @@ import android.hardware.biometrics.fingerprint.ISession; import android.hardware.fingerprint.Fingerprint; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.TestableContext; import androidx.annotation.NonNull; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; +import com.android.server.biometrics.Flags; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.sensors.ClientMonitorCallback; @@ -69,6 +74,10 @@ public class FingerprintInternalCleanupClientTest { public final TestableContext mContext = new TestableContext( InstrumentationRegistry.getInstrumentation().getTargetContext(), null); + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + @Mock ISession mSession; @Mock @@ -168,6 +177,21 @@ public class FingerprintInternalCleanupClientTest { assertThat(mClient.getUnknownHALTemplates()).isEmpty(); } + @Test + @RequiresFlagsEnabled(Flags.FLAG_NOTIFY_FINGERPRINT_LOE) + public void invalidBiometricUserState() throws Exception { + mClient = createClient(); + + final List<Fingerprint> list = new ArrayList<>(); + doReturn(true).when(mFingerprintUtils) + .hasValidBiometricUserState(mContext, 2); + doReturn(list).when(mFingerprintUtils).getBiometricsForUser(mContext, 2); + + mClient.start(mCallback); + mClient.onEnumerationResult(null, 0); + verify(mFingerprintUtils).deleteStateForUser(2); + } + protected FingerprintInternalCleanupClient createClient() { final Map<Integer, Long> authenticatorIds = new HashMap<>(); return new FingerprintInternalCleanupClient(mContext, () -> mAidlSession, 2 /* userId */, diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java index e5c42082ab97..fb82b872cf80 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java @@ -888,7 +888,7 @@ public class ManagedServicesTest extends UiServiceTestCase { return true; }); - mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>()); + mockServiceInfoWithMetaData(List.of(cn), service, pm, new ArrayMap<>()); service.addApprovedList("a", 0, true); service.reregisterService(cn, 0); @@ -919,7 +919,7 @@ public class ManagedServicesTest extends UiServiceTestCase { return true; }); - mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>()); + mockServiceInfoWithMetaData(List.of(cn), service, pm, new ArrayMap<>()); service.addApprovedList("a", 0, false); service.reregisterService(cn, 0); @@ -950,7 +950,7 @@ public class ManagedServicesTest extends UiServiceTestCase { return true; }); - mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>()); + mockServiceInfoWithMetaData(List.of(cn), service, pm, new ArrayMap<>()); service.addApprovedList("a/a", 0, true); service.reregisterService(cn, 0); @@ -981,7 +981,7 @@ public class ManagedServicesTest extends UiServiceTestCase { return true; }); - mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>()); + mockServiceInfoWithMetaData(List.of(cn), service, pm, new ArrayMap<>()); service.addApprovedList("a/a", 0, false); service.reregisterService(cn, 0); @@ -1211,6 +1211,64 @@ public class ManagedServicesTest extends UiServiceTestCase { } @Test + public void testUpgradeAppNoIntentFilterNoRebind() throws Exception { + Context context = spy(getContext()); + doReturn(true).when(context).bindServiceAsUser(any(), any(), anyInt(), any()); + + ManagedServices service = new TestManagedServices(context, mLock, mUserProfiles, + mIpm, APPROVAL_BY_COMPONENT); + + List<String> packages = new ArrayList<>(); + packages.add("package"); + addExpectedServices(service, packages, 0); + + final ComponentName unapprovedComponent = ComponentName.unflattenFromString("package/C1"); + final ComponentName approvedComponent = ComponentName.unflattenFromString("package/C2"); + + // Both components are approved initially + mExpectedPrimaryComponentNames.clear(); + mExpectedPrimaryPackages.clear(); + mExpectedPrimaryComponentNames.put(0, "package/C1:package/C2"); + mExpectedSecondaryComponentNames.clear(); + mExpectedSecondaryPackages.clear(); + + loadXml(service); + + //Component package/C1 loses serviceInterface intent filter + ManagedServices.Config config = service.getConfig(); + when(mPm.queryIntentServicesAsUser(any(), anyInt(), anyInt())). + thenAnswer(new Answer<List<ResolveInfo>>() { + @Override + public List<ResolveInfo> answer(InvocationOnMock invocationOnMock) + throws Throwable { + Object[] args = invocationOnMock.getArguments(); + Intent invocationIntent = (Intent) args[0]; + if (invocationIntent != null) { + if (invocationIntent.getAction().equals(config.serviceInterface) + && packages.contains(invocationIntent.getPackage())) { + List<ResolveInfo> dummyServices = new ArrayList<>(); + ResolveInfo resolveInfo = new ResolveInfo(); + ServiceInfo serviceInfo = new ServiceInfo(); + serviceInfo.packageName = invocationIntent.getPackage(); + serviceInfo.name = approvedComponent.getClassName(); + serviceInfo.permission = service.getConfig().bindPermission; + resolveInfo.serviceInfo = serviceInfo; + dummyServices.add(resolveInfo); + return dummyServices; + } + } + return new ArrayList<>(); + } + }); + + // Trigger package update + service.onPackagesChanged(false, new String[]{"package"}, new int[]{0}); + + assertFalse(service.isComponentEnabledForCurrentProfiles(unapprovedComponent)); + assertTrue(service.isComponentEnabledForCurrentProfiles(approvedComponent)); + } + + @Test public void testSetPackageOrComponentEnabled() throws Exception { for (int approvalLevel : new int[] {APPROVAL_BY_COMPONENT, APPROVAL_BY_PACKAGE}) { ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, @@ -1915,7 +1973,7 @@ public class ManagedServicesTest extends UiServiceTestCase { metaDataAutobindAllow.putBoolean(META_DATA_DEFAULT_AUTOBIND, true); metaDatas.put(cn_allowed, metaDataAutobindAllow); - mockServiceInfoWithMetaData(componentNames, service, metaDatas); + mockServiceInfoWithMetaData(componentNames, service, pm, metaDatas); service.addApprovedList(cn_allowed.flattenToString(), 0, true); service.addApprovedList(cn_disallowed.flattenToString(), 0, true); @@ -1960,7 +2018,7 @@ public class ManagedServicesTest extends UiServiceTestCase { metaDataAutobindDisallow.putBoolean(META_DATA_DEFAULT_AUTOBIND, false); metaDatas.put(cn_disallowed, metaDataAutobindDisallow); - mockServiceInfoWithMetaData(componentNames, service, metaDatas); + mockServiceInfoWithMetaData(componentNames, service, pm, metaDatas); service.addApprovedList(cn_disallowed.flattenToString(), 0, true); @@ -1999,7 +2057,7 @@ public class ManagedServicesTest extends UiServiceTestCase { metaDataAutobindDisallow.putBoolean(META_DATA_DEFAULT_AUTOBIND, false); metaDatas.put(cn_disallowed, metaDataAutobindDisallow); - mockServiceInfoWithMetaData(componentNames, service, metaDatas); + mockServiceInfoWithMetaData(componentNames, service, pm, metaDatas); service.addApprovedList(cn_disallowed.flattenToString(), 0, true); @@ -2070,8 +2128,8 @@ public class ManagedServicesTest extends UiServiceTestCase { } private void mockServiceInfoWithMetaData(List<ComponentName> componentNames, - ManagedServices service, ArrayMap<ComponentName, Bundle> metaDatas) - throws RemoteException { + ManagedServices service, PackageManager packageManager, + ArrayMap<ComponentName, Bundle> metaDatas) throws RemoteException { when(mIpm.getServiceInfo(any(), anyLong(), anyInt())).thenAnswer( (Answer<ServiceInfo>) invocation -> { ComponentName invocationCn = invocation.getArgument(0); @@ -2086,6 +2144,39 @@ public class ManagedServicesTest extends UiServiceTestCase { return null; } ); + + // add components to queryIntentServicesAsUser response + final List<String> packages = new ArrayList<>(); + for (ComponentName cn: componentNames) { + packages.add(cn.getPackageName()); + } + ManagedServices.Config config = service.getConfig(); + when(packageManager.queryIntentServicesAsUser(any(), anyInt(), anyInt())). + thenAnswer(new Answer<List<ResolveInfo>>() { + @Override + public List<ResolveInfo> answer(InvocationOnMock invocationOnMock) + throws Throwable { + Object[] args = invocationOnMock.getArguments(); + Intent invocationIntent = (Intent) args[0]; + if (invocationIntent != null) { + if (invocationIntent.getAction().equals(config.serviceInterface) + && packages.contains(invocationIntent.getPackage())) { + List<ResolveInfo> dummyServices = new ArrayList<>(); + for (ComponentName cn: componentNames) { + ResolveInfo resolveInfo = new ResolveInfo(); + ServiceInfo serviceInfo = new ServiceInfo(); + serviceInfo.packageName = invocationIntent.getPackage(); + serviceInfo.name = cn.getClassName(); + serviceInfo.permission = service.getConfig().bindPermission; + resolveInfo.serviceInfo = serviceInfo; + dummyServices.add(resolveInfo); + } + return dummyServices; + } + } + return new ArrayList<>(); + } + }); } private void resetComponentsAndPackages() { diff --git a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java index 901c0361092f..4f7593184d83 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java @@ -20,7 +20,7 @@ import static android.os.VibrationAttributes.CATEGORY_KEYBOARD; import static android.os.VibrationAttributes.CATEGORY_UNKNOWN; import static android.os.VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY; import static android.os.VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF; -import static android.os.VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE; +import static android.os.VibrationAttributes.USAGE_IME_FEEDBACK; import static android.os.VibrationAttributes.USAGE_TOUCH; import static android.os.VibrationEffect.Composition.PRIMITIVE_CLICK; import static android.os.VibrationEffect.Composition.PRIMITIVE_TICK; @@ -346,7 +346,7 @@ public class HapticFeedbackVibrationProviderTest { } @Test - public void testVibrationAttribute_keyboardCategoryOff_isIme_notUseKeyboardCategory() { + public void testVibrationAttribute_keyboardCategoryOff_isIme_useTouchUsage() { mSetFlagsRule.disableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED); HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations(); @@ -362,7 +362,7 @@ public class HapticFeedbackVibrationProviderTest { } @Test - public void testVibrationAttribute_keyboardCategoryOn_notIme_notUseKeyboardCategory() { + public void testVibrationAttribute_keyboardCategoryOn_notIme_useTouchUsage() { mSetFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED); HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations(); @@ -377,7 +377,7 @@ public class HapticFeedbackVibrationProviderTest { } @Test - public void testVibrationAttribute_keyboardCategoryOn_isIme_useKeyboardCategory() { + public void testVibrationAttribute_keyboardCategoryOn_isIme_useImeFeedbackUsage() { mSetFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED); HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations(); @@ -385,64 +385,14 @@ public class HapticFeedbackVibrationProviderTest { VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback( effectId, /* flags */ 0, HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS); - assertWithMessage("Expected USAGE_TOUCH for effect " + effectId) - .that(attrs.getUsage()).isEqualTo(USAGE_TOUCH); + assertWithMessage("Expected USAGE_IME_FEEDBACK for effect " + effectId) + .that(attrs.getUsage()).isEqualTo(USAGE_IME_FEEDBACK); assertWithMessage("Expected CATEGORY_KEYBOARD for effect " + effectId) .that(attrs.getCategory()).isEqualTo(CATEGORY_KEYBOARD); } } @Test - public void testVibrationAttribute_noFixAmplitude_notBypassIntensityScale() { - mSetFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED); - mockVibratorPrimitiveSupport(PRIMITIVE_CLICK, PRIMITIVE_TICK); - mockKeyboardVibrationFixedAmplitude(-1); - HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations(); - - for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) { - VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback( - effectId, /* flags */ 0, - HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS); - assertWithMessage("Expected no FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE for effect " - + effectId) - .that(attrs.isFlagSet(FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)).isFalse(); - } - } - - @Test - public void testVibrationAttribute_notIme_notBypassIntensityScale() { - mSetFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED); - mockVibratorPrimitiveSupport(PRIMITIVE_CLICK, PRIMITIVE_TICK); - mockKeyboardVibrationFixedAmplitude(KEYBOARD_VIBRATION_FIXED_AMPLITUDE); - HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations(); - - for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) { - VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback( - effectId, /* flags */ 0, /* privFlags */ 0); - assertWithMessage("Expected no FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE for effect " - + effectId) - .that(attrs.isFlagSet(FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)).isFalse(); - } - } - - @Test - public void testVibrationAttribute_fixAmplitude_isIme_bypassIntensityScale() { - mSetFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED); - mockVibratorPrimitiveSupport(PRIMITIVE_CLICK, PRIMITIVE_TICK); - mockKeyboardVibrationFixedAmplitude(KEYBOARD_VIBRATION_FIXED_AMPLITUDE); - HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations(); - - for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) { - VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback( - effectId, /* flags */ 0, - HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS); - assertWithMessage("Expected FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE for effect " - + effectId) - .that(attrs.isFlagSet(FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)).isTrue(); - } - } - - @Test public void testIsRestricted_biometricConstants_returnsTrue() { HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations(); diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java index 60d8964267f1..8d4a6aa5ba29 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java @@ -23,6 +23,7 @@ import static android.os.VibrationAttributes.USAGE_ACCESSIBILITY; import static android.os.VibrationAttributes.USAGE_ALARM; import static android.os.VibrationAttributes.USAGE_COMMUNICATION_REQUEST; import static android.os.VibrationAttributes.USAGE_HARDWARE_FEEDBACK; +import static android.os.VibrationAttributes.USAGE_IME_FEEDBACK; import static android.os.VibrationAttributes.USAGE_MEDIA; import static android.os.VibrationAttributes.USAGE_NOTIFICATION; import static android.os.VibrationAttributes.USAGE_PHYSICAL_EMULATION; @@ -893,6 +894,22 @@ public class VibrationSettingsTest { } @Test + public void getCurrentIntensity_ImeFeedbackValueReflectsToKeyboardVibrationSettings() { + setDefaultIntensity(USAGE_IME_FEEDBACK, VIBRATION_INTENSITY_MEDIUM); + setDefaultIntensity(USAGE_TOUCH, VIBRATION_INTENSITY_HIGH); + + setKeyboardVibrationSettingsSupported(false); + mVibrationSettings.update(); + assertEquals(VIBRATION_INTENSITY_HIGH, + mVibrationSettings.getCurrentIntensity(USAGE_IME_FEEDBACK)); + + setKeyboardVibrationSettingsSupported(true); + mVibrationSettings.update(); + assertEquals(VIBRATION_INTENSITY_MEDIUM, + mVibrationSettings.getCurrentIntensity(USAGE_IME_FEEDBACK)); + } + + @Test public void getFallbackEffect_returnsEffectsFromSettings() { assertNotNull(mVibrationSettings.getFallbackEffect(VibrationEffect.EFFECT_TICK)); assertNotNull(mVibrationSettings.getFallbackEffect(VibrationEffect.EFFECT_TEXTURE_TICK)); diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java index bea691783a8a..e411a178eca4 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java @@ -154,6 +154,9 @@ public class VibratorManagerServiceTest { private static final VibrationAttributes RINGTONE_ATTRS = new VibrationAttributes.Builder().setUsage( VibrationAttributes.USAGE_RINGTONE).build(); + private static final VibrationAttributes IME_FEEDBACK_ATTRS = + new VibrationAttributes.Builder().setUsage( + VibrationAttributes.USAGE_IME_FEEDBACK).build(); private static final VibrationAttributes UNKNOWN_ATTRS = new VibrationAttributes.Builder().setUsage(VibrationAttributes.USAGE_UNKNOWN).build(); @@ -853,6 +856,7 @@ public class VibratorManagerServiceTest { vibrate(service, VibrationEffect.createOneShot(2000, 200), new VibrationAttributes.Builder().setUsage( VibrationAttributes.USAGE_UNKNOWN).build()); + vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), IME_FEEDBACK_ATTRS); InOrder inOrderVerifier = inOrder(mAppOpsManagerMock); inOrderVerifier.verify(mAppOpsManagerMock).checkAudioOpNoThrow(eq(AppOpsManager.OP_VIBRATE), @@ -868,6 +872,8 @@ public class VibratorManagerServiceTest { anyInt(), anyString()); inOrderVerifier.verify(mAppOpsManagerMock).checkAudioOpNoThrow(eq(AppOpsManager.OP_VIBRATE), eq(AudioAttributes.USAGE_UNKNOWN), anyInt(), anyString()); + inOrderVerifier.verify(mAppOpsManagerMock).checkAudioOpNoThrow(eq(AppOpsManager.OP_VIBRATE), + eq(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION), anyInt(), anyString()); } @Test @@ -1684,40 +1690,6 @@ public class VibratorManagerServiceTest { } @Test - public void vibrate_withBypassScaleFlag_ignoresIntensitySettingsAndResolvesAmplitude() - throws Exception { - // Permission needed for bypassing user settings - grantPermission(android.Manifest.permission.MODIFY_PHONE_STATE); - - int defaultTouchIntensity = - mVibrator.getDefaultVibrationIntensity(VibrationAttributes.USAGE_TOUCH); - // This will scale down touch vibrations. - setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, - defaultTouchIntensity > Vibrator.VIBRATION_INTENSITY_LOW - ? defaultTouchIntensity - 1 - : defaultTouchIntensity); - - int defaultAmplitude = mContextSpy.getResources().getInteger( - com.android.internal.R.integer.config_defaultVibrationAmplitude); - - mockVibrators(1); - FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1); - fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); - VibratorManagerService service = createSystemReadyService(); - - vibrateAndWaitUntilFinished(service, - VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE), - new VibrationAttributes.Builder() - .setUsage(VibrationAttributes.USAGE_TOUCH) - .setFlags(VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE) - .build()); - - assertEquals(1, fakeVibrator.getAllEffectSegments().size()); - - assertEquals(defaultAmplitude / 255f, fakeVibrator.getAmplitudes().get(0), 1e-5); - } - - @Test @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) public void vibrate_withAdaptiveHaptics_appliesCorrectAdaptiveScales() throws Exception { // Keep user settings the same as device default so only adaptive scale is applied. diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java index ff1c6c8fc70c..d0080d29f82b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java @@ -102,6 +102,7 @@ import android.provider.DeviceConfig; import android.service.voice.IVoiceInteractionSession; import android.util.Pair; import android.util.Size; +import android.view.Display; import android.view.Gravity; import android.view.RemoteAnimationAdapter; import android.window.TaskFragmentOrganizerToken; @@ -941,6 +942,91 @@ public class ActivityStarterTests extends WindowTestsBase { notNull() /* options */); } + + /** + * This test ensures that activity launch on a secondary display is allowed if the activity did + * not opt out from showing on remote devices. + */ + @Test + public void testStartActivityOnVirtualDisplay() { + final ActivityStarter starter = prepareStarter(FLAG_ACTIVITY_NEW_TASK, + false /* mockGetRootTask */); + starter.mRequest.activityInfo.flags |= ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES; + + // Create a virtual display at bottom. + final TestDisplayContent secondaryDisplay = + new TestDisplayContent.Builder(mAtm, 1000, 1500) + .setType(Display.TYPE_VIRTUAL) + .setPosition(POSITION_BOTTOM).build(); + final TaskDisplayArea secondaryTaskContainer = secondaryDisplay.getDefaultTaskDisplayArea(); + final Task stack = secondaryTaskContainer.createRootTask( + WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */); + + // Create an activity record on the top of secondary display. + final ActivityRecord topActivityOnSecondaryDisplay = createSingleTaskActivityOn(stack); + + // Put an activity on default display as the top focused activity. + new ActivityBuilder(mAtm).setCreateTask(true).build(); + + // Start activity with the same intent as {@code topActivityOnSecondaryDisplay} + // on secondary display. + final ActivityOptions options = ActivityOptions.makeBasic() + .setLaunchDisplayId(secondaryDisplay.mDisplayId); + final int result = starter.setReason("testStartActivityOnVirtualDisplay") + .setIntent(topActivityOnSecondaryDisplay.intent) + .setActivityOptions(options.toBundle()) + .execute(); + + // Ensure result is delivering intent to top. + assertEquals(START_DELIVERED_TO_TOP, result); + + // Ensure secondary display only creates one stack. + verify(secondaryTaskContainer, times(1)).createRootTask(anyInt(), anyInt(), anyBoolean()); + } + + /** + * This test ensures that activity launch on a secondary display is disallowed if the activity + * opted out from showing on remote devices. + */ + @EnableFlags(android.companion.virtualdevice.flags.Flags + .FLAG_ENFORCE_REMOTE_DEVICE_OPT_OUT_ON_ALL_VIRTUAL_DISPLAYS) + @Test + public void testStartOptedOutActivityOnVirtualDisplay() { + final ActivityStarter starter = prepareStarter(FLAG_ACTIVITY_NEW_TASK, + false /* mockGetRootTask */); + starter.mRequest.activityInfo.flags &= ~ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES; + + // Create a virtual display at bottom. + final TestDisplayContent secondaryDisplay = + new TestDisplayContent.Builder(mAtm, 1000, 1500) + .setType(Display.TYPE_VIRTUAL) + .setPosition(POSITION_BOTTOM).build(); + final TaskDisplayArea secondaryTaskContainer = secondaryDisplay.getDefaultTaskDisplayArea(); + final Task stack = secondaryTaskContainer.createRootTask( + WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */); + + // Create an activity record on the top of secondary display. + final ActivityRecord topActivityOnSecondaryDisplay = createSingleTaskActivityOn(stack); + + // Put an activity on default display as the top focused activity. + new ActivityBuilder(mAtm).setCreateTask(true).build(); + + // Start activity with the same intent as {@code topActivityOnSecondaryDisplay} + // on secondary display. + final ActivityOptions options = ActivityOptions.makeBasic() + .setLaunchDisplayId(secondaryDisplay.mDisplayId); + final int result = starter.setReason("testStartOptedOutActivityOnVirtualDisplay") + .setIntent(topActivityOnSecondaryDisplay.intent) + .setActivityOptions(options.toBundle()) + .execute(); + + // Ensure result is canceled. + assertEquals(START_CANCELED, result); + + // Ensure secondary display only creates one stack. + verify(secondaryTaskContainer, times(1)).createRootTask(anyInt(), anyInt(), anyBoolean()); + } + @Test public void testWasVisibleInRestartAttempt() { final ActivityStarter starter = prepareStarter( diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java index afa22bc5eae8..a159ce354a7b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java @@ -72,7 +72,6 @@ import android.window.TaskSnapshot; import android.window.WindowOnBackInvokedDispatcher; import com.android.server.LocalServices; -import com.android.window.flags.Flags; import org.junit.Before; import org.junit.Test; @@ -672,7 +671,6 @@ public class BackNavigationControllerTests extends WindowTestsBase { @Test public void testBackOnMostRecentWindowInActivityEmbedding() { - mSetFlagsRule.enableFlags(Flags.FLAG_EMBEDDED_ACTIVITY_BACK_NAV_FLAG); final Task task = createTask(mDefaultDisplay); final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); final TaskFragment primaryTf = createTaskFragmentWithEmbeddedActivity(task, organizer); diff --git a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerExemptionTests.java b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerExemptionTests.java index 366e519fb063..6e488188eb87 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerExemptionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerExemptionTests.java @@ -16,6 +16,9 @@ package com.android.server.wm; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; + import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_ALLOW; import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_ALLOWLISTED_COMPONENT; import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_FOREGROUND; @@ -23,7 +26,6 @@ import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_ import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_SAW_PERMISSION; import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_VISIBLE_WINDOW; -import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.ArgumentMatchers.anyInt; @@ -58,7 +60,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.quality.Strictness; import java.lang.reflect.Field; @@ -134,9 +135,8 @@ public class BackgroundActivityStartControllerExemptionTests { ActivityOptions mCheckedOptions = ActivityOptions.makeBasic() .setPendingIntentCreatorBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) - .setPendingIntentBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); + MODE_BACKGROUND_ACTIVITY_START_ALLOWED) + .setPendingIntentBackgroundActivityStartMode(MODE_BACKGROUND_ACTIVITY_START_ALLOWED); class TestableBackgroundActivityStartController extends BackgroundActivityStartController { private Set<Pair<Integer, Integer>> mBalPermissionUidPidPairs = new HashSet<>(); @@ -175,7 +175,6 @@ public class BackgroundActivityStartControllerExemptionTests { when(mService.getAppOpsManager()).thenReturn(mAppOpsManager); setViaReflection(mService, "mProcessMap", mProcessMap); - //Mockito.when(mSupervisor.getBackgroundActivityLaunchController()).thenReturn(mController); setViaReflection(mSupervisor, "mRecentTasks", mRecentTasks); mController = new TestableBackgroundActivityStartController(mService, mSupervisor); @@ -397,7 +396,7 @@ public class BackgroundActivityStartControllerExemptionTests { // setup state WindowProcessControllerMap mProcessMap = new WindowProcessControllerMap(); - WindowProcessController otherProcess = Mockito.mock(WindowProcessController.class); + WindowProcessController otherProcess = mock(WindowProcessController.class); mProcessMap.put(callingPid, mCallerApp); mProcessMap.put(REGULAR_PID_1_1, otherProcess); setViaReflection(mService, "mProcessMap", mProcessMap); @@ -516,14 +515,13 @@ public class BackgroundActivityStartControllerExemptionTests { BackgroundStartPrivileges forcedBalByPiSender = BackgroundStartPrivileges.NONE; Intent intent = TEST_INTENT; ActivityOptions checkedOptions = mCheckedOptions; - checkedOptions.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); + checkedOptions.setPendingIntentBackgroundActivityStartMode( + MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); BackgroundActivityStartController.BalState balState = mController.new BalState(callingUid, callingPid, callingPackage, realCallingUid, realCallingPid, null, originatingPendingIntent, forcedBalByPiSender, mResultRecord, intent, checkedOptions); - assertThat(balState.isPendingIntentBalAllowedByPermission()).isTrue(); - // call BalVerdict realCallerVerdict = mController.checkBackgroundActivityStartAllowedBySender( balState); diff --git a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java index f110c69b57f5..e364264fc74f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java @@ -547,7 +547,7 @@ public class BackgroundActivityStartControllerTests { assertThat(balState.callerExplicitOptInOrOut()).isFalse(); assertThat(balState.realCallerExplicitOptInOrAutoOptIn()).isTrue(); assertThat(balState.realCallerExplicitOptInOrOut()).isFalse(); - assertThat(balState.toString()).contains( + assertThat(balState.toString()).startsWith( "[callingPackage: package.app1; " + "callingPackageTargetSdk: -1; " + "callingUid: 10001; " @@ -563,6 +563,7 @@ public class BackgroundActivityStartControllerTests { + "balAllowedByPiCreator: BSP.ALLOW_BAL; " + "balAllowedByPiCreatorWithHardening: BSP.ALLOW_BAL; " + "resultIfPiCreatorAllowsBal: null; " + + "callerStartMode: MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; " + "hasRealCaller: true; " + "isCallForResult: false; " + "isPendingIntent: false; " @@ -646,7 +647,7 @@ public class BackgroundActivityStartControllerTests { assertThat(balState.callerExplicitOptInOrOut()).isFalse(); assertThat(balState.realCallerExplicitOptInOrAutoOptIn()).isFalse(); assertThat(balState.realCallerExplicitOptInOrOut()).isFalse(); - assertThat(balState.toString()).contains( + assertThat(balState.toString()).startsWith( "[callingPackage: package.app1; " + "callingPackageTargetSdk: -1; " + "callingUid: 10001; " @@ -662,6 +663,7 @@ public class BackgroundActivityStartControllerTests { + "balAllowedByPiCreator: BSP.NONE; " + "balAllowedByPiCreatorWithHardening: BSP.NONE; " + "resultIfPiCreatorAllowsBal: null; " + + "callerStartMode: MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; " + "hasRealCaller: true; " + "isCallForResult: false; " + "isPendingIntent: true; " diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java index 57118f236815..f84338656436 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java @@ -63,11 +63,6 @@ public class DisplayContentDeferredUpdateTests extends WindowTestsBase { private final Message mScreenUnblocker = mock(Message.class); - @Override - protected void onBeforeSystemServicesCreated() { - mSetFlagsRule.enableFlags(Flags.FLAG_DEFER_DISPLAY_UPDATES); - } - @Before public void before() { doReturn(true).when(mDisplayContent).getLastHasContent(); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java index 3fcf3042ab94..2e0d4d46ec05 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java @@ -24,12 +24,15 @@ import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; + import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeTrue; +import static org.mockito.ArgumentMatchers.eq; import static org.testng.Assert.assertFalse; import android.annotation.Nullable; @@ -58,6 +61,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.util.List; import java.util.function.Consumer; /** @@ -352,20 +356,58 @@ public class DisplayWindowSettingsProviderTests extends WindowTestsBase { } @Test - public void testRemovesStaleDisplaySettings() { + public void testRemovesStaleDisplaySettings_defaultDisplay_removesStaleDisplaySettings() { assumeTrue(com.android.window.flags.Flags.perUserDisplayWindowSettings()); - final DisplayWindowSettingsProvider provider = - new DisplayWindowSettingsProvider(mDefaultVendorSettingsStorage, - mOverrideSettingsStorage); - final DisplayInfo displayInfo = mSecondaryDisplay.getDisplayInfo(); - updateOverrideSettings(provider, displayInfo, settings -> settings.mForcedDensity = 356); + // Write density setting for second display then remove it. + final DisplayWindowSettingsProvider provider = new DisplayWindowSettingsProvider( + mDefaultVendorSettingsStorage, mOverrideSettingsStorage); + final DisplayInfo secDisplayInfo = mSecondaryDisplay.getDisplayInfo(); + updateOverrideSettings(provider, secDisplayInfo, setting -> setting.mForcedDensity = 356); mRootWindowContainer.removeChild(mSecondaryDisplay); - provider.removeStaleDisplaySettings(mRootWindowContainer); + // Write density setting for inner and outer default display. + final DisplayInfo innerDisplayInfo = mPrimaryDisplay.getDisplayInfo(); + final DisplayInfo outerDisplayInfo = new DisplayInfo(secDisplayInfo); + outerDisplayInfo.displayId = mPrimaryDisplay.mDisplayId; + outerDisplayInfo.uniqueId = "TEST_OUTER_DISPLAY_" + System.currentTimeMillis(); + updateOverrideSettings(provider, innerDisplayInfo, setting -> setting.mForcedDensity = 490); + updateOverrideSettings(provider, outerDisplayInfo, setting -> setting.mForcedDensity = 420); + final List<DisplayInfo> possibleDisplayInfos = List.of(innerDisplayInfo, outerDisplayInfo); + doReturn(possibleDisplayInfos) + .when(mWm).getPossibleDisplayInfoLocked(eq(innerDisplayInfo.displayId)); + + provider.removeStaleDisplaySettingsLocked(mWm, mRootWindowContainer); + + assertThat(mOverrideSettingsStorage.wasWriteSuccessful()).isTrue(); + assertThat(provider.getOverrideSettingsSize()).isEqualTo(2); + assertThat(provider.getOverrideSettings(innerDisplayInfo).mForcedDensity).isEqualTo(490); + assertThat(provider.getOverrideSettings(outerDisplayInfo).mForcedDensity).isEqualTo(420); + } + + @Test + public void testRemovesStaleDisplaySettings_displayNotInLayout_keepsDisplaySettings() { + assumeTrue(com.android.window.flags.Flags.perUserDisplayWindowSettings()); + + // Write density setting for primary display. + final DisplayWindowSettingsProvider provider = new DisplayWindowSettingsProvider( + mDefaultVendorSettingsStorage, mOverrideSettingsStorage); + final DisplayInfo primDisplayInfo = mPrimaryDisplay.getDisplayInfo(); + updateOverrideSettings(provider, primDisplayInfo, setting -> setting.mForcedDensity = 420); + + // Add a virtual display and write density setting for it. + final DisplayInfo virtDisplayInfo = new DisplayInfo(primDisplayInfo); + virtDisplayInfo.uniqueId = "TEST_VIRTUAL_DISPLAY_" + System.currentTimeMillis(); + createNewDisplay(virtDisplayInfo); + waitUntilHandlersIdle(); // Wait until unfrozen after a display is added. + updateOverrideSettings(provider, virtDisplayInfo, setting -> setting.mForcedDensity = 490); + + provider.removeStaleDisplaySettingsLocked(mWm, mRootWindowContainer); assertThat(mOverrideSettingsStorage.wasWriteSuccessful()).isTrue(); - assertThat(provider.getOverrideSettingsSize()).isEqualTo(0); + assertThat(provider.getOverrideSettingsSize()).isEqualTo(2); + assertThat(provider.getOverrideSettings(primDisplayInfo).mForcedDensity).isEqualTo(420); + assertThat(provider.getOverrideSettings(virtDisplayInfo).mForcedDensity).isEqualTo(490); } /** diff --git a/services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java b/services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java deleted file mode 100644 index 78509dbfdb1a..000000000000 --- a/services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java +++ /dev/null @@ -1,283 +0,0 @@ -/* - * 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.server.wm; - -import static com.android.internal.R.bool.config_unfoldTransitionEnabled; -import static com.android.server.wm.DeviceStateController.DeviceState.REAR; -import static com.android.server.wm.DeviceStateController.DeviceState.FOLDED; -import static com.android.server.wm.DeviceStateController.DeviceState.HALF_FOLDED; -import static com.android.server.wm.DeviceStateController.DeviceState.OPEN; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.when; - -import android.animation.ValueAnimator; -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Rect; -import android.platform.test.annotations.Presubmit; - -import androidx.test.filters.SmallTest; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -/** - * Tests for the {@link WindowToken} class. - * - * Build/Install/Run: - * atest WmTests:PhysicalDisplaySwitchTransitionLauncherTest - */ -@SmallTest -@Presubmit -@RunWith(WindowTestRunner.class) -public class PhysicalDisplaySwitchTransitionLauncherTest extends WindowTestsBase { - - @Mock - Context mContext; - @Mock - Resources mResources; - @Mock - BLASTSyncEngine mSyncEngine; - - WindowTestsBase.TestTransitionPlayer mPlayer; - TransitionController mTransitionController; - DisplayContent mDisplayContent; - - private PhysicalDisplaySwitchTransitionLauncher mTarget; - private float mOriginalAnimationScale; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - mTransitionController = new WindowTestsBase.TestTransitionController(mAtm); - mTransitionController.setSyncEngine(mSyncEngine); - mPlayer = new WindowTestsBase.TestTransitionPlayer( - mTransitionController, mAtm.mWindowOrganizerController); - when(mContext.getResources()).thenReturn(mResources); - mDisplayContent = new TestDisplayContent.Builder(mAtm, 100, 150).build(); - mTarget = new PhysicalDisplaySwitchTransitionLauncher(mDisplayContent, mAtm, mContext, - mTransitionController); - mOriginalAnimationScale = ValueAnimator.getDurationScale(); - } - - @After - public void after() { - ValueAnimator.setDurationScale(mOriginalAnimationScale); - } - - @Test - public void testDisplaySwitchAfterUnfoldToOpen_animationsEnabled_requestsTransition() { - givenAllAnimationsEnabled(); - mTarget.foldStateChanged(FOLDED); - - mTarget.foldStateChanged(OPEN); - final Rect origBounds = new Rect(); - mDisplayContent.getBounds(origBounds); - origBounds.offsetTo(0, 0); - mTarget.requestDisplaySwitchTransitionIfNeeded( - mDisplayContent.getDisplayId(), - origBounds.width(), - origBounds.height(), - /* newDisplayWidth= */ 200, - /* newDisplayHeight= */ 250 - ); - - assertNotNull(mPlayer.mLastRequest); - assertEquals(mDisplayContent.getDisplayId(), - mPlayer.mLastRequest.getDisplayChange().getDisplayId()); - assertEquals(origBounds, mPlayer.mLastRequest.getDisplayChange().getStartAbsBounds()); - assertEquals(new Rect(0, 0, 200, 250), - mPlayer.mLastRequest.getDisplayChange().getEndAbsBounds()); - } - - @Test - public void testDisplaySwitchAfterFolding_animationEnabled_doesNotRequestTransition() { - givenAllAnimationsEnabled(); - mTarget.foldStateChanged(OPEN); - - mTarget.foldStateChanged(FOLDED); - requestDisplaySwitch(); - - assertTransitionNotRequested(); - } - - @Test - public void testDisplaySwitchAfterUnfoldingToHalf_animationEnabled_requestsTransition() { - givenAllAnimationsEnabled(); - mTarget.foldStateChanged(FOLDED); - - mTarget.foldStateChanged(HALF_FOLDED); - requestDisplaySwitch(); - - assertTransitionRequested(); - } - - @Test - public void testDisplaySwitchSecondTimeAfterUnfolding_animationEnabled_noTransition() { - givenAllAnimationsEnabled(); - mTarget.foldStateChanged(FOLDED); - mTarget.foldStateChanged(OPEN); - requestDisplaySwitch(); - mPlayer.mLastRequest = null; - - requestDisplaySwitch(); - - assertTransitionNotRequested(); - } - - - @Test - public void testDisplaySwitchAfterGoingToRearAndBack_animationEnabled_noTransition() { - givenAllAnimationsEnabled(); - mTarget.foldStateChanged(OPEN); - - mTarget.foldStateChanged(REAR); - mTarget.foldStateChanged(OPEN); - requestDisplaySwitch(); - - assertTransitionNotRequested(); - } - - @Test - public void testDisplaySwitchAfterUnfoldingAndFolding_animationEnabled_noTransition() { - givenAllAnimationsEnabled(); - mTarget.foldStateChanged(FOLDED); - mTarget.foldStateChanged(OPEN); - // No request display switch event (simulate very fast fold after unfold, even before - // the displays switched) - mTarget.foldStateChanged(FOLDED); - - requestDisplaySwitch(); - - assertTransitionNotRequested(); - } - - @Test - public void testDisplaySwitch_whenShellTransitionsNotEnabled_noTransition() { - givenAllAnimationsEnabled(); - givenShellTransitionsEnabled(false); - mTarget.foldStateChanged(FOLDED); - - mTarget.foldStateChanged(OPEN); - requestDisplaySwitch(); - - assertTransitionNotRequested(); - } - - @Test - public void testDisplaySwitch_whenAnimationsDisabled_noTransition() { - givenAllAnimationsEnabled(); - givenAnimationsEnabled(false); - mTarget.foldStateChanged(FOLDED); - - mTarget.foldStateChanged(OPEN); - requestDisplaySwitch(); - - assertTransitionNotRequested(); - } - - @Test - public void testDisplaySwitch_whenUnfoldAnimationDisabled_noTransition() { - givenAllAnimationsEnabled(); - givenUnfoldTransitionEnabled(false); - mTarget.foldStateChanged(FOLDED); - - mTarget.foldStateChanged(OPEN); - requestDisplaySwitch(); - - assertTransitionNotRequested(); - } - - @Test - public void testDisplaySwitchAfterUnfolding_otherCollectingTransition_collectsDisplaySwitch() { - givenAllAnimationsEnabled(); - mTarget.foldStateChanged(FOLDED); - - mTarget.foldStateChanged(OPEN); - requestDisplaySwitch(); - - // Collects to the current transition - assertTrue(mTransitionController.getCollectingTransition().mParticipants.contains( - mDisplayContent)); - } - - - @Test - public void testDisplaySwitch_whenNoContentInDisplayContent_noTransition() { - givenAllAnimationsEnabled(); - givenDisplayContentHasContent(false); - mTarget.foldStateChanged(FOLDED); - - mTarget.foldStateChanged(OPEN); - requestDisplaySwitch(); - - assertTransitionNotRequested(); - } - - private void assertTransitionRequested() { - assertNotNull(mPlayer.mLastRequest); - } - - private void assertTransitionNotRequested() { - assertNull(mPlayer.mLastRequest); - } - - private void requestDisplaySwitch() { - mTarget.requestDisplaySwitchTransitionIfNeeded( - mDisplayContent.getDisplayId(), - mDisplayContent.getBounds().width(), - mDisplayContent.getBounds().height(), - /* newDisplayWidth= */ 200, - /* newDisplayHeight= */ 250 - ); - } - - private void givenAllAnimationsEnabled() { - givenAnimationsEnabled(true); - givenUnfoldTransitionEnabled(true); - givenShellTransitionsEnabled(true); - givenDisplayContentHasContent(true); - } - - private void givenUnfoldTransitionEnabled(boolean enabled) { - when(mResources.getBoolean(config_unfoldTransitionEnabled)).thenReturn(enabled); - } - - private void givenAnimationsEnabled(boolean enabled) { - ValueAnimator.setDurationScale(enabled ? 1.0f : 0.0f); - } - - private void givenShellTransitionsEnabled(boolean enabled) { - if (enabled) { - mTransitionController.registerTransitionPlayer(mPlayer, null /* proc */); - } else { - mTransitionController.unregisterTransitionPlayer(mPlayer); - } - } - - private void givenDisplayContentHasContent(boolean hasContent) { - when(mDisplayContent.getLastHasContent()).thenReturn(hasContent); - } -} diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java index 3c247a03d744..6be1af2c143f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java @@ -995,16 +995,14 @@ public class TaskFragmentTest extends WindowTestsBase { // The focus should change. assertEquals(winLeftTop, mDisplayContent.mCurrentFocus); - if (Flags.embeddedActivityBackNavFlag()) { - // Move focus if the adjacent activity is more recently active. - doReturn(1L).when(appLeftTop).getLastWindowCreateTime(); - doReturn(2L).when(appRightTop).getLastWindowCreateTime(); - assertTrue(mWm.moveFocusToAdjacentEmbeddedWindow(winLeftTop)); - - // Do not move the focus if the adjacent activity is less recently active. - doReturn(3L).when(appLeftTop).getLastWindowCreateTime(); - assertFalse(mWm.moveFocusToAdjacentEmbeddedWindow(winLeftTop)); - } + // Move focus if the adjacent activity is more recently active. + doReturn(1L).when(appLeftTop).getLastWindowCreateTime(); + doReturn(2L).when(appRightTop).getLastWindowCreateTime(); + assertTrue(mWm.moveFocusToAdjacentEmbeddedWindow(winLeftTop)); + + // Do not move the focus if the adjacent activity is less recently active. + doReturn(3L).when(appLeftTop).getLastWindowCreateTime(); + assertFalse(mWm.moveFocusToAdjacentEmbeddedWindow(winLeftTop)); } @Test diff --git a/tests/Input/src/com/android/test/input/InputEventAssignerTest.kt b/tests/Input/src/com/android/test/input/InputEventAssignerTest.kt index c1a86b3a2dac..015e188fc98e 100644 --- a/tests/Input/src/com/android/test/input/InputEventAssignerTest.kt +++ b/tests/Input/src/com/android/test/input/InputEventAssignerTest.kt @@ -18,12 +18,20 @@ package com.android.test.input import android.view.InputDevice.SOURCE_MOUSE import android.view.InputDevice.SOURCE_TOUCHSCREEN +import android.view.InputDevice.SOURCE_STYLUS +import android.view.InputDevice.SOURCE_TOUCHPAD + import android.view.InputEventAssigner import android.view.KeyEvent import android.view.MotionEvent import org.junit.Assert.assertEquals import org.junit.Test +sealed class StreamEvent +private data object Vsync : StreamEvent() +data class MotionEventData(val action: Int, val source: Int, val id: Int, val expectedId: Int) : + StreamEvent() + /** * Create a MotionEvent with the provided action, eventTime, and source */ @@ -49,64 +57,164 @@ private fun createKeyEvent(action: Int, eventTime: Long): KeyEvent { return KeyEvent(eventTime, eventTime, action, code, repeat) } +/** + * Check that the correct eventIds are assigned in a stream. The stream consists of motion + * events or vsync (processed frame) + * Each streamEvent should have unique ids when writing tests + * The test passes even if two events get assigned the same eventId, since the mapping is + * streamEventId -> motionEventId and streamEvents have unique ids + */ +private fun checkEventStream(vararg streamEvents: StreamEvent) { + val assigner = InputEventAssigner() + var eventTime = 10L + // Maps MotionEventData.id to MotionEvent.id + // We can't control the event id of the generated motion events but for testing it's easier + // to label the events with a custom id for readability + val eventIdMap: HashMap<Int, Int> = HashMap() + for (streamEvent in streamEvents) { + when (streamEvent) { + is MotionEventData -> { + val event = createMotionEvent(streamEvent.action, eventTime, streamEvent.source) + eventIdMap[streamEvent.id] = event.id + val eventId = assigner.processEvent(event) + assertEquals(eventIdMap[streamEvent.expectedId], eventId) + } + is Vsync -> assigner.notifyFrameProcessed() + } + eventTime += 1 + } +} + class InputEventAssignerTest { companion object { private const val TAG = "InputEventAssignerTest" } /** - * A single MOVE event should be assigned to the next available frame. + * A single event should be assigned to the next available frame. */ @Test - fun testTouchGesture() { - val assigner = InputEventAssigner() - val event = createMotionEvent(MotionEvent.ACTION_MOVE, 10, SOURCE_TOUCHSCREEN) - val eventId = assigner.processEvent(event) - assertEquals(event.id, eventId) + fun testTouchMove() { + checkEventStream( + MotionEventData(MotionEvent.ACTION_MOVE, SOURCE_TOUCHSCREEN, id = 1, expectedId = 1) + ) + } + + @Test + fun testMouseMove() { + checkEventStream( + MotionEventData(MotionEvent.ACTION_MOVE, SOURCE_MOUSE, id = 1, expectedId = 1) + ) + } + + @Test + fun testMouseScroll() { + checkEventStream( + MotionEventData(MotionEvent.ACTION_SCROLL, SOURCE_MOUSE, id = 1, expectedId = 1) + ) + } + + @Test + fun testStylusMove() { + checkEventStream( + MotionEventData(MotionEvent.ACTION_MOVE, SOURCE_STYLUS, id = 1, expectedId = 1) + ) + } + + @Test + fun testStylusHover() { + checkEventStream( + MotionEventData(MotionEvent.ACTION_HOVER_MOVE, SOURCE_STYLUS, id = 1, expectedId = 1) + ) + } + + @Test + fun testTouchpadMove() { + checkEventStream( + MotionEventData(MotionEvent.ACTION_MOVE, SOURCE_STYLUS, id = 1, expectedId = 1) + ) } /** - * DOWN event should be used until a vsync comes in. After vsync, the latest event should be - * produced. + * Test that before a VSYNC the event id generated by input event assigner for move events is + * the id of the down event. Move events coming after a VSYNC should be assigned their own event + * id */ + private fun testDownAndMove(source: Int) { + checkEventStream( + MotionEventData(MotionEvent.ACTION_DOWN, source, id = 1, expectedId = 1), + MotionEventData(MotionEvent.ACTION_MOVE, source, id = 2, expectedId = 1), + Vsync, + MotionEventData(MotionEvent.ACTION_MOVE, source, id = 4, expectedId = 4) + ) + } + @Test - fun testTouchDownWithMove() { - val assigner = InputEventAssigner() - val down = createMotionEvent(MotionEvent.ACTION_DOWN, 10, SOURCE_TOUCHSCREEN) - val move1 = createMotionEvent(MotionEvent.ACTION_MOVE, 12, SOURCE_TOUCHSCREEN) - val move2 = createMotionEvent(MotionEvent.ACTION_MOVE, 13, SOURCE_TOUCHSCREEN) - val move3 = createMotionEvent(MotionEvent.ACTION_MOVE, 14, SOURCE_TOUCHSCREEN) - val move4 = createMotionEvent(MotionEvent.ACTION_MOVE, 15, SOURCE_TOUCHSCREEN) - var eventId = assigner.processEvent(down) - assertEquals(down.id, eventId) - eventId = assigner.processEvent(move1) - assertEquals(down.id, eventId) - eventId = assigner.processEvent(move2) - // Even though we already had 2 move events, there was no choreographer callback yet. - // Therefore, we should still get the id of the down event - assertEquals(down.id, eventId) + fun testTouchDownAndMove() { + testDownAndMove(SOURCE_TOUCHSCREEN) + } - // Now send CALLBACK_INPUT to the assigner. It should provide the latest motion event - assigner.notifyFrameProcessed() - eventId = assigner.processEvent(move3) - assertEquals(move3.id, eventId) - eventId = assigner.processEvent(move4) - assertEquals(move4.id, eventId) + @Test + fun testMouseDownAndMove() { + testDownAndMove(SOURCE_MOUSE) + } + + @Test + fun testStylusDownAndMove() { + testDownAndMove(SOURCE_STYLUS) + } + + @Test + fun testTouchpadDownAndMove() { + testDownAndMove(SOURCE_TOUCHPAD) } /** - * Similar to the above test, but with SOURCE_MOUSE. Since we don't have down latency - * concept for non-touchscreens, the latest input event will be used. + * After an up event, motion events should be assigned their own event id */ @Test - fun testMouseDownWithMove() { - val assigner = InputEventAssigner() - val down = createMotionEvent(MotionEvent.ACTION_DOWN, 10, SOURCE_MOUSE) - val move1 = createMotionEvent(MotionEvent.ACTION_MOVE, 12, SOURCE_MOUSE) - var eventId = assigner.processEvent(down) - assertEquals(down.id, eventId) - eventId = assigner.processEvent(move1) - assertEquals(move1.id, eventId) + fun testMouseDownUpAndScroll() { + checkEventStream( + MotionEventData(MotionEvent.ACTION_DOWN, SOURCE_MOUSE, id = 1, expectedId = 1), + MotionEventData(MotionEvent.ACTION_UP, SOURCE_MOUSE, id = 2, expectedId = 2), + MotionEventData(MotionEvent.ACTION_SCROLL, SOURCE_MOUSE, id = 3, expectedId = 3) + ) + } + + /** + * After an up event, motion events should be assigned their own event id + */ + @Test + fun testStylusDownUpAndHover() { + checkEventStream( + MotionEventData(MotionEvent.ACTION_DOWN, SOURCE_STYLUS, id = 1, expectedId = 1), + MotionEventData(MotionEvent.ACTION_UP, SOURCE_STYLUS, id = 2, expectedId = 2), + MotionEventData(MotionEvent.ACTION_HOVER_ENTER, SOURCE_STYLUS, id = 3, expectedId = 3) + ) + } + + /** + * After a cancel event, motion events should be assigned their own event id + */ + @Test + fun testMouseDownCancelAndScroll() { + checkEventStream( + MotionEventData(MotionEvent.ACTION_DOWN, SOURCE_MOUSE, id = 1, expectedId = 1), + MotionEventData(MotionEvent.ACTION_CANCEL, SOURCE_MOUSE, id = 2, expectedId = 2), + MotionEventData(MotionEvent.ACTION_SCROLL, SOURCE_MOUSE, id = 3, expectedId = 3) + ) + } + + /** + * After a cancel event, motion events should be assigned their own event id + */ + @Test + fun testStylusDownCancelAndHover() { + checkEventStream( + MotionEventData(MotionEvent.ACTION_DOWN, SOURCE_STYLUS, id = 1, expectedId = 1), + MotionEventData(MotionEvent.ACTION_CANCEL, SOURCE_STYLUS, id = 2, expectedId = 2), + MotionEventData(MotionEvent.ACTION_HOVER_ENTER, SOURCE_STYLUS, id = 3, expectedId = 3) + ) } /** |