Merge "Log RemoteInputActive value for entry and all" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index d554347..6bd6c93 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -25,10 +25,12 @@
         ":android.view.flags-aconfig-java{.generated_srcjars}",
         ":camera_platform_flags_core_java_lib{.generated_srcjars}",
         ":com.android.window.flags.window-aconfig-java{.generated_srcjars}",
+        ":com.android.hardware.input-aconfig-java{.generated_srcjars}",
         ":com.android.text.flags-aconfig-java{.generated_srcjars}",
         ":telecom_flags_core_java_lib{.generated_srcjars}",
         ":android.companion.virtual.flags-aconfig-java{.generated_srcjars}",
         ":android.view.inputmethod.flags-aconfig-java{.generated_srcjars}",
+        ":android.widget.flags-aconfig-java{.generated_srcjars}",
     ],
     // Add aconfig-annotations-lib as a dependency for the optimization
     libs: ["aconfig-annotations-lib"],
@@ -69,6 +71,19 @@
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
 
+// Input
+aconfig_declarations {
+    name: "com.android.hardware.input.input-aconfig",
+    package: "com.android.hardware.input",
+    srcs: ["core/java/android/hardware/input/*.aconfig"],
+}
+
+java_aconfig_library {
+    name: "com.android.hardware.input-aconfig-java",
+    aconfig_declarations: "com.android.hardware.input.input-aconfig",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
 // Text
 aconfig_declarations {
     name: "com.android.text.flags-aconfig",
@@ -185,3 +200,17 @@
     aconfig_declarations: "android.view.flags-aconfig",
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
+
+// Widget
+aconfig_declarations {
+    name: "android.widget.flags-aconfig",
+    package: "android.widget.flags",
+    srcs: ["core/java/android/widget/flags/*.aconfig"],
+}
+
+java_aconfig_library {
+    name: "android.widget.flags-aconfig-java",
+    aconfig_declarations: "android.widget.flags-aconfig",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
diff --git a/core/api/current.txt b/core/api/current.txt
index 14ddf40..8de8ab8 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -50040,6 +50040,13 @@
     field public static final int VIRTUAL_KEY_RELEASE = 8; // 0x8
   }
 
+  @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) public class HapticScrollFeedbackProvider implements android.view.ScrollFeedbackProvider {
+    ctor public HapticScrollFeedbackProvider(@NonNull android.view.View);
+    method public void onScrollLimit(int, int, int, boolean);
+    method public void onScrollProgress(int, int, int, int);
+    method public void onSnapToItem(int, int, int);
+  }
+
   public class InflateException extends java.lang.RuntimeException {
     ctor public InflateException();
     ctor public InflateException(String, Throwable);
@@ -51205,6 +51212,15 @@
     method @UiThread public void updatePositionInWindow();
   }
 
+  @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) public interface ScrollFeedbackProvider {
+    method public void onScrollLimit(int, int, int, boolean);
+    method public default void onScrollLimit(@NonNull android.view.MotionEvent, int, boolean);
+    method public void onScrollProgress(int, int, int, int);
+    method public default void onScrollProgress(@NonNull android.view.MotionEvent, int, int);
+    method public void onSnapToItem(int, int, int);
+    method public default void onSnapToItem(@NonNull android.view.MotionEvent, int);
+  }
+
   public class SearchEvent {
     ctor public SearchEvent(android.view.InputDevice);
     method public android.view.InputDevice getInputDevice();
@@ -52482,6 +52498,7 @@
     method @Deprecated public static int getEdgeSlop();
     method @Deprecated public static int getFadingEdgeLength();
     method @Deprecated public static long getGlobalActionKeyTimeout();
+    method @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) public int getHapticScrollFeedbackTickInterval(int, int, int);
     method public static int getJumpTapTimeout();
     method public static int getKeyRepeatDelay();
     method public static int getKeyRepeatTimeout();
@@ -52521,6 +52538,7 @@
     method @Deprecated public static int getWindowTouchSlop();
     method public static long getZoomControlsTimeout();
     method public boolean hasPermanentMenuKey();
+    method @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) public boolean isHapticScrollFeedbackEnabled(int, int, int);
     method public boolean shouldShowMenuShortcutsWhenKeyboardPresent();
   }
 
diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig
new file mode 100644
index 0000000..ebfe66f5
--- /dev/null
+++ b/core/java/android/hardware/input/input_framework.aconfig
@@ -0,0 +1,19 @@
+package: "com.android.hardware.input"
+
+# Project link: https://gantry.corp.google.com/projects/android_platform_input_native/changes
+
+flag {
+    namespace: "input_native"
+    name: "keyboard_layout_preview_flag"
+    description: "Controls whether a preview will be shown in Settings when selecting a physical keyboard layout"
+    bug: "293579375"
+}
+
+
+flag {
+    namespace: "input_native"
+    name: "keyboard_a11y_sticky_keys_flag"
+    description: "Controls if the sticky keys accessibility feature for physical keyboard is available to the user"
+    bug: "294546335"
+}
+
diff --git a/core/java/android/view/HapticScrollFeedbackProvider.java b/core/java/android/view/HapticScrollFeedbackProvider.java
index 7e103a5..fba23ba 100644
--- a/core/java/android/view/HapticScrollFeedbackProvider.java
+++ b/core/java/android/view/HapticScrollFeedbackProvider.java
@@ -16,7 +16,9 @@
 
 package android.view;
 
-import static com.android.internal.R.dimen.config_rotaryEncoderAxisScrollTickInterval;
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.view.flags.Flags;
 
 import com.android.internal.annotations.VisibleForTesting;
 
@@ -25,16 +27,15 @@
  *
  * <p>Each scrolling widget should have its own instance of this class to ensure that scroll state
  * is isolated.
- *
- * @hide
  */
+@FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API)
 public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider {
     private static final String TAG = "HapticScrollFeedbackProvider";
 
     private static final int TICK_INTERVAL_NO_TICK = 0;
-    private static final int TICK_INTERVAL_UNSET = Integer.MAX_VALUE;
 
     private final View mView;
+    private final ViewConfiguration mViewConfig;
 
 
     // Info about the cause of the latest scroll event.
@@ -49,26 +50,35 @@
      * Cache for tick interval for scroll tick caused by a {@link InputDevice#SOURCE_ROTARY_ENCODER}
      * on {@link MotionEvent#AXIS_SCROLL}. Set to -1 if the value has not been fetched and cached.
      */
-    private int mRotaryEncoderAxisScrollTickIntervalPixels = TICK_INTERVAL_UNSET;
     /** The tick interval corresponding to the current InputDevice/source/axis. */
     private int mTickIntervalPixels = TICK_INTERVAL_NO_TICK;
     private int mTotalScrollPixels = 0;
     private boolean mCanPlayLimitFeedback = true;
+    private boolean mHapticScrollFeedbackEnabled = false;
 
-    public HapticScrollFeedbackProvider(View view) {
-        this(view, /* rotaryEncoderAxisScrollTickIntervalPixels= */ TICK_INTERVAL_UNSET);
+    public HapticScrollFeedbackProvider(@NonNull View view) {
+        this(view, ViewConfiguration.get(view.getContext()));
     }
 
     /** @hide */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
-    public HapticScrollFeedbackProvider(View view, int rotaryEncoderAxisScrollTickIntervalPixels) {
+    public HapticScrollFeedbackProvider(View view, ViewConfiguration viewConfig) {
         mView = view;
-        mRotaryEncoderAxisScrollTickIntervalPixels = rotaryEncoderAxisScrollTickIntervalPixels;
+        mViewConfig = viewConfig;
     }
 
     @Override
-    public void onScrollProgress(MotionEvent event, int axis, int deltaInPixels) {
-        maybeUpdateCurrentConfig(event, axis);
+    public void onScrollProgress(int inputDeviceId, int source, int axis, int deltaInPixels) {
+        maybeUpdateCurrentConfig(inputDeviceId, source, axis);
+        if (!mHapticScrollFeedbackEnabled) {
+            return;
+        }
+
+        // Unlock limit feedback regardless of scroll tick being enabled as long as there's a
+        // non-zero scroll progress.
+        if (deltaInPixels != 0) {
+            mCanPlayLimitFeedback = true;
+        }
 
         if (mTickIntervalPixels == TICK_INTERVAL_NO_TICK) {
             // There's no valid tick interval. Exit early before doing any further computation.
@@ -82,13 +92,14 @@
             // TODO(b/239594271): create a new `performHapticFeedbackForDevice` and use that here.
             mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_TICK);
         }
-
-        mCanPlayLimitFeedback = true;
     }
 
     @Override
-    public void onScrollLimit(MotionEvent event, int axis, boolean isStart) {
-        maybeUpdateCurrentConfig(event, axis);
+    public void onScrollLimit(int inputDeviceId, int source, int axis, boolean isStart) {
+        maybeUpdateCurrentConfig(inputDeviceId, source, axis);
+        if (!mHapticScrollFeedbackEnabled) {
+            return;
+        }
 
         if (!mCanPlayLimitFeedback) {
             return;
@@ -101,41 +112,33 @@
     }
 
     @Override
-    public void onSnapToItem(MotionEvent event, int axis) {
+    public void onSnapToItem(int inputDeviceId, int source, int axis) {
+        maybeUpdateCurrentConfig(inputDeviceId, source, axis);
+        if (!mHapticScrollFeedbackEnabled) {
+            return;
+        }
         // TODO(b/239594271): create a new `performHapticFeedbackForDevice` and use that here.
         mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_ITEM_FOCUS);
         mCanPlayLimitFeedback = true;
     }
 
-    private void maybeUpdateCurrentConfig(MotionEvent event, int axis) {
-        int source = event.getSource();
-        int deviceId = event.getDeviceId();
-
+    private void maybeUpdateCurrentConfig(int deviceId, int source, int axis) {
         if (mAxis != axis || mSource != source || mDeviceId != deviceId) {
             mSource = source;
             mAxis = axis;
             mDeviceId = deviceId;
 
+            mHapticScrollFeedbackEnabled =
+                    mViewConfig.isHapticScrollFeedbackEnabled(deviceId, axis, source);
             mCanPlayLimitFeedback = true;
             mTotalScrollPixels = 0;
-            calculateTickIntervals(source, axis);
+            updateTickIntervals(deviceId, source, axis);
         }
     }
 
-    private void calculateTickIntervals(int source, int axis) {
-        mTickIntervalPixels = TICK_INTERVAL_NO_TICK;
-
-        if (axis == MotionEvent.AXIS_SCROLL && source == InputDevice.SOURCE_ROTARY_ENCODER) {
-            if (mRotaryEncoderAxisScrollTickIntervalPixels == TICK_INTERVAL_UNSET) {
-                // Value has not been fetched  yet. Fetch and cache it.
-                mRotaryEncoderAxisScrollTickIntervalPixels =
-                        mView.getContext().getResources().getDimensionPixelSize(
-                                config_rotaryEncoderAxisScrollTickInterval);
-                if (mRotaryEncoderAxisScrollTickIntervalPixels < 0) {
-                    mRotaryEncoderAxisScrollTickIntervalPixels = TICK_INTERVAL_NO_TICK;
-                }
-            }
-            mTickIntervalPixels = mRotaryEncoderAxisScrollTickIntervalPixels;
-        }
+    private void updateTickIntervals(int deviceId, int source, int axis) {
+        mTickIntervalPixels = mHapticScrollFeedbackEnabled
+                ? mViewConfig.getHapticScrollFeedbackTickInterval(deviceId, axis, source)
+                : TICK_INTERVAL_NO_TICK;
     }
 }
diff --git a/core/java/android/view/ScrollFeedbackProvider.java b/core/java/android/view/ScrollFeedbackProvider.java
index 8d3491d..6f760c5 100644
--- a/core/java/android/view/ScrollFeedbackProvider.java
+++ b/core/java/android/view/ScrollFeedbackProvider.java
@@ -16,16 +16,45 @@
 
 package android.view;
 
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.view.flags.Flags;
+
 /**
  * Interface to represent an entity giving consistent feedback for different events surrounding view
  * scroll.
  *
- * @hide
+ * <p>When you have access to the {@link MotionEvent}s that triggered the different scroll events,
+ * use the {@link MotionEvent} based APIs in this class. If you do not have access to the motion
+ * events, you can use the methods that accept the {@link InputDevice} ID (which can be obtained by
+ * APIs like {@link MotionEvent#getDeviceId()} and {@link InputDevice#getId()}) and source (which
+ * can be obtained by APIs like {@link MotionEvent#getSource()}) of the motion that caused the
+ * scroll events.
  */
+@FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API)
 public interface ScrollFeedbackProvider {
     /**
-     * The view has snapped to an item, with a motion from a given {@link MotionEvent} on a given
-     * {@code axis}.
+     * Call this when the view has snapped to an item, with a motion generated by an
+     * {@link InputDevice} with an id of {@code inputDeviceId}, from an input {@code source} and on
+     * a given motion event {@code axis}.
+     *
+     * <p>This method has the same purpose as {@link #onSnapToItem(MotionEvent, int)}. When a scroll
+     * snap happens, call either this method or {@link #onSnapToItem(MotionEvent, int)}, not both.
+     * This method is useful when you have no direct access to the {@link MotionEvent} that
+     * caused the snap event.
+     *
+     * @param inputDeviceId the ID of the {@link InputDevice} that generated the motion triggering
+     *          the snap.
+     * @param source the input source of the motion causing the snap.
+     * @param axis the axis of {@code event} that caused the item to snap.
+     *
+     * @see #onSnapToItem(MotionEvent, int)
+     */
+    void onSnapToItem(int inputDeviceId, int source, int axis);
+
+    /**
+     * Call this when the view has snapped to an item, with a motion from a given
+     * {@link MotionEvent} on a given {@code axis}.
      *
      * <p>The interface is not aware of the internal scroll states of the view for which scroll
      * feedback is played. As such, the client should call
@@ -33,22 +62,69 @@
      *
      * @param event the {@link MotionEvent} that caused the item to snap.
      * @param axis the axis of {@code event} that caused the item to snap.
+     *
+     * @see #onSnapToItem(int, int, int)
      */
-    void onSnapToItem(MotionEvent event, int axis);
+    default void onSnapToItem(@NonNull MotionEvent event, int axis) {
+        onSnapToItem(event.getDeviceId(), event.getSource(), axis);
+    }
 
     /**
-     * The view has reached the scroll limit when scrolled by the motion from a given
+     * Call this when the view has reached the scroll limit when scrolled by a motion generated by
+     * an {@link InputDevice} with an id of {@code inputDeviceId}, from an input {@code source} and
+     * on a given motion event {@code axis}.
+     *
+     * <p>This method has the same purpose as {@link #onScrollLimit(MotionEvent, int, boolean)}.
+     * When a scroll limit happens, call either this method or
+     * {@link #onScrollLimit(MotionEvent, int, boolean)}, not both. This method is useful when you
+     * have no direct access to the {@link MotionEvent} that caused the scroll limit.
+     *
+     * @param inputDeviceId the ID of the {@link InputDevice} that caused scrolling to hit limit.
+     * @param source the input source of the motion that caused scrolling to hit the limit.
+     * @param axis the axis of {@code event} that caused scrolling to hit the limit.
+     * @param isStart {@code true} if scrolling hit limit at the start of the scrolling list, and
+     *                {@code false} if the scrolling hit limit at the end of the scrolling list.
+     *
+     * @see #onScrollLimit(MotionEvent, int, boolean)
+     */
+    void onScrollLimit(int inputDeviceId, int source, int axis, boolean isStart);
+
+    /**
+     * Call this when the view has reached the scroll limit when scrolled by the motion from a given
      * {@link MotionEvent} on a given {@code axis}.
      *
      * @param event the {@link MotionEvent} that caused scrolling to hit the limit.
      * @param axis the axis of {@code event} that caused scrolling to hit the limit.
      * @param isStart {@code true} if scrolling hit limit at the start of the scrolling list, and
      *                {@code false} if the scrolling hit limit at the end of the scrolling list.
+     *
+     * @see #onScrollLimit(int, int, int, boolean)
      */
-    void onScrollLimit(MotionEvent event, int axis, boolean isStart);
+    default void onScrollLimit(@NonNull MotionEvent event, int axis, boolean isStart) {
+        onScrollLimit(event.getDeviceId(), event.getSource(), axis, isStart);
+    }
 
     /**
-     * The view has scrolled by {@code deltaInPixels} due to the motion from a given
+     * Call this when the view has scrolled by {@code deltaInPixels} due to the motion generated by
+     * an {@link InputDevice} with an id of {@code inputDeviceId}, from an input {@code source} and
+     * on a given motion event {@code axis}.
+     *
+     * <p>This method has the same purpose as {@link #onScrollProgress(MotionEvent, int, int)}.
+     * When a scroll progress happens, call either this method or
+     * {@link #onScrollProgress(MotionEvent, int, int)}, not both. This method is useful when you
+     * have no direct access to the {@link MotionEvent} that caused the scroll progress.
+     *
+     * @param inputDeviceId the ID of the {@link InputDevice} that caused scroll progress.
+     * @param source the input source of the motion that caused scroll progress.
+     * @param axis the axis of {@code event} that caused scroll progress.
+     * @param deltaInPixels the amount of scroll progress, in pixels.
+     *
+     * @see #onScrollProgress(MotionEvent, int, int)
+     */
+    void onScrollProgress(int inputDeviceId, int source, int axis, int deltaInPixels);
+
+    /**
+     * Call this when the view has scrolled by {@code deltaInPixels} due to the motion from a given
      * {@link MotionEvent} on a given {@code axis}.
      *
      * <p>The interface is not aware of the internal scroll states of the view for which scroll
@@ -58,6 +134,10 @@
      * @param event the {@link MotionEvent} that caused scroll progress.
      * @param axis the axis of {@code event} that caused scroll progress.
      * @param deltaInPixels the amount of scroll progress, in pixels.
+     *
+     * @see #onScrollProgress(int, int, int, int)
      */
-    void onScrollProgress(MotionEvent event, int axis, int deltaInPixels);
+    default void onScrollProgress(@NonNull MotionEvent event, int axis, int deltaInPixels) {
+        onScrollProgress(event.getDeviceId(), event.getSource(), axis, deltaInPixels);
+    }
 }
diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java
index 2a88cf0..0244d46 100644
--- a/core/java/android/view/ViewConfiguration.java
+++ b/core/java/android/view/ViewConfiguration.java
@@ -16,6 +16,7 @@
 
 package android.view;
 
+import android.annotation.FlaggedApi;
 import android.annotation.FloatRange;
 import android.annotation.NonNull;
 import android.annotation.TestApi;
@@ -37,6 +38,7 @@
 import android.util.DisplayMetrics;
 import android.util.SparseArray;
 import android.util.TypedValue;
+import android.view.flags.Flags;
 
 /**
  * Contains methods to standard constants used in the UI for timeouts, sizes, and distances.
@@ -240,6 +242,9 @@
     /** Value used as a maximum fling velocity, when fling is not supported. */
     private static final int NO_FLING_MAX_VELOCITY = Integer.MIN_VALUE;
 
+    /** @hide */
+    public static final int NO_HAPTIC_SCROLL_TICK_INTERVAL = Integer.MAX_VALUE;
+
     /**
      * Delay before dispatching a recurring accessibility event in milliseconds.
      * This delay guarantees that a recurring event will be send at most once
@@ -343,6 +348,8 @@
     private final int mMaximumFlingVelocity;
     private final int mMinimumRotaryEncoderFlingVelocity;
     private final int mMaximumRotaryEncoderFlingVelocity;
+    private final int mRotaryEncoderHapticScrollFeedbackTickIntervalPixels;
+    private final boolean mRotaryEncoderHapticScrollFeedbackEnabled;
     private final int mScrollbarSize;
     private final int mTouchSlop;
     private final int mHandwritingSlop;
@@ -390,6 +397,8 @@
         mMaximumFlingVelocity = MAXIMUM_FLING_VELOCITY;
         mMinimumRotaryEncoderFlingVelocity = MINIMUM_FLING_VELOCITY;
         mMaximumRotaryEncoderFlingVelocity = MAXIMUM_FLING_VELOCITY;
+        mRotaryEncoderHapticScrollFeedbackEnabled = false;
+        mRotaryEncoderHapticScrollFeedbackTickIntervalPixels = NO_HAPTIC_SCROLL_TICK_INTERVAL;
         mScrollbarSize = SCROLL_BAR_SIZE;
         mTouchSlop = TOUCH_SLOP;
         mHandwritingSlop = HANDWRITING_SLOP;
@@ -529,6 +538,20 @@
             mMaximumRotaryEncoderFlingVelocity = configMaxRotaryEncoderFlingVelocity;
         }
 
+        int configRotaryEncoderHapticScrollFeedbackTickIntervalPixels =
+                res.getDimensionPixelSize(
+                        com.android.internal.R.dimen
+                                .config_rotaryEncoderAxisScrollTickInterval);
+        mRotaryEncoderHapticScrollFeedbackTickIntervalPixels =
+                configRotaryEncoderHapticScrollFeedbackTickIntervalPixels > 0
+                        ? configRotaryEncoderHapticScrollFeedbackTickIntervalPixels
+                        : NO_HAPTIC_SCROLL_TICK_INTERVAL;
+
+        mRotaryEncoderHapticScrollFeedbackEnabled =
+                res.getBoolean(
+                        com.android.internal.R.bool
+                                .config_viewRotaryEncoderHapticScrollFedbackEnabled);
+
         mGlobalActionsKeyTimeout = res.getInteger(
                 com.android.internal.R.integer.config_globalActionsKeyTimeout);
 
@@ -1193,6 +1216,93 @@
         return mMaximumFlingVelocity;
     }
 
+    /**
+     * Checks if any kind of scroll haptic feedback is enabled for a motion generated by a specific
+     * input device configuration and motion axis.
+     *
+     * <h3>Obtaining the correct arguments for this method call</h3>
+     * <p><b>inputDeviceId</b>: if calling this method in response to a {@link MotionEvent}, use
+     * the device ID that is reported by the event, which can be obtained using
+     * {@link MotionEvent#getDeviceId()}. Otherwise, use a valid ID that is obtained from
+     * {@link InputDevice#getId()}, or from an {@link InputManager} instance
+     * ({@link InputManager#getInputDeviceIds()} gives all the valid input device IDs).
+     *
+     * <p><b>axis</b>: a {@link MotionEvent} may report data for multiple axes, and each axis may
+     * have multiple data points for different pointers. Use the axis whose movement produced the
+     * scrolls that would generate the scroll haptics. You can use
+     * {@link InputDevice#getMotionRanges()} to get all the {@link InputDevice.MotionRange}s for the
+     * {@link InputDevice}, from which you can derive all the valid axes for the device.
+     *
+     * <p><b>source</b>: use {@link MotionEvent#getSource()} if calling this method in response to a
+     * {@link MotionEvent}. Otherwise, use a valid source for the {@link InputDevice}. You can use
+     * {@link InputDevice#getMotionRanges()} to get all the {@link InputDevice.MotionRange}s for the
+     * {@link InputDevice}, from which you can derive all the valid sources for the device.
+     *
+     * @param inputDeviceId the ID of the {@link InputDevice} that generated the motion that may
+     *      produce scroll haptics.
+     * @param source the input source of the motion that may produce scroll haptics.
+     * @param axis the axis of the motion that may produce scroll haptics.
+     * @return {@code true} if motions generated by the provided input and motion configuration
+     *      should produce scroll haptics. {@code false} otherwise.
+     */
+    @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API)
+    public boolean isHapticScrollFeedbackEnabled(int inputDeviceId, int axis, int source) {
+        if (!isInputDeviceInfoValid(inputDeviceId, axis, source)) return false;
+
+        if (source == InputDevice.SOURCE_ROTARY_ENCODER) {
+            return mRotaryEncoderHapticScrollFeedbackEnabled;
+        }
+
+        return false;
+    }
+
+    /**
+     * Provides the minimum scroll interval (in pixels) between consecutive scroll tick haptics for
+     * motions generated by a specific input device configuration and motion axis.
+     *
+     * <p><b>Scroll tick</b> here refers to an interval-based, consistent scroll feedback provided
+     * to the user as the user scrolls through a scrollable view.
+     *
+     * <p>If you are supporting scroll tick haptics, use this interval as the minimum pixel scroll
+     * distance between consecutive scroll ticks. That is, once your view has scrolled for at least
+     * this interval, play a haptic, and wait again until the view has further scrolled with this
+     * interval in the same direction before playing the next scroll haptic.
+     *
+     * <p>Some devices may support other types of scroll haptics but not interval based tick
+     * haptics. In those cases, this method will return {@code Integer.MAX_VALUE}. The same value
+     * will be returned if the device does not support scroll haptics at all (which can be checked
+     * via {@link #isHapticScrollFeedbackEnabled(int, int, int)}).
+     *
+     * <p>See {@link #isHapticScrollFeedbackEnabled(int, int, int)} for more details about obtaining
+     * the correct arguments for this method.
+     *
+     * @param inputDeviceId the ID of the {@link InputDevice} that generated the motion that may
+     *      produce scroll haptics.
+     * @param source the input source of the motion that may produce scroll haptics.
+     * @param axis the axis of the motion that may produce scroll haptics.
+     * @return the absolute value of the minimum scroll interval, in pixels, between consecutive
+     *      scroll feedback haptics for motions generated by the provided input and motion
+     *      configuration. If scroll haptics is disabled for the given configuration, or if the
+     *      device does not support scroll tick haptics for the given configuration, this method
+     *      returns {@code Integer.MAX_VALUE}.
+     */
+    @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API)
+    public int getHapticScrollFeedbackTickInterval(int inputDeviceId, int axis, int source) {
+        if (!mRotaryEncoderHapticScrollFeedbackEnabled) {
+            return NO_HAPTIC_SCROLL_TICK_INTERVAL;
+        }
+
+        if (!isInputDeviceInfoValid(inputDeviceId, axis, source)) {
+            return NO_HAPTIC_SCROLL_TICK_INTERVAL;
+        }
+
+        if (source == InputDevice.SOURCE_ROTARY_ENCODER) {
+            return mRotaryEncoderHapticScrollFeedbackTickIntervalPixels;
+        }
+
+        return NO_HAPTIC_SCROLL_TICK_INTERVAL;
+    }
+
     private static boolean isInputDeviceInfoValid(int id, int axis, int source) {
         InputDevice device = InputManagerGlobal.getInstance().getInputDevice(id);
         return device != null && device.getMotionRange(axis, source) != null;
diff --git a/core/java/android/view/flags/scroll_feedback_flags.aconfig b/core/java/android/view/flags/scroll_feedback_flags.aconfig
new file mode 100644
index 0000000..62c5691
--- /dev/null
+++ b/core/java/android/view/flags/scroll_feedback_flags.aconfig
@@ -0,0 +1,8 @@
+package: "android.view.flags"
+
+flag {
+    namespace: "toolkit"
+    name: "scroll_feedback_api"
+    description: "Enable the scroll feedback APIs"
+    bug: "239594271"
+}
\ No newline at end of file
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index 03364b6..a116542 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -53,6 +53,7 @@
 import android.view.ContextMenu.ContextMenuInfo;
 import android.view.Gravity;
 import android.view.HapticFeedbackConstants;
+import android.view.HapticScrollFeedbackProvider;
 import android.view.InputDevice;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
@@ -91,6 +92,7 @@
 import android.view.inputmethod.SurroundingText;
 import android.view.inspector.InspectableProperty;
 import android.view.inspector.InspectableProperty.EnumEntry;
+import android.widget.flags.Flags;
 import android.widget.RemoteViews.InteractionHandler;
 
 import com.android.internal.R;
@@ -918,6 +920,8 @@
 
     private DifferentialMotionFlingHelper mDifferentialMotionFlingHelper;
 
+    private HapticScrollFeedbackProvider mHapticScrollFeedbackProvider;
+
     public AbsListView(Context context) {
         super(context);
         setupDeviceConfigProperties();
@@ -4502,10 +4506,6 @@
                 final float axisValue = (axis == -1) ? 0 : event.getAxisValue(axis);
                 final int delta = Math.round(axisValue * mVerticalScrollFactor);
                 if (delta != 0) {
-                    // Tracks whether or not we should attempt fling for this event.
-                    // Fling should not be attempted if the view is already at the limit of scroll,
-                    // since it conflicts with EdgeEffect.
-                    boolean shouldAttemptFling = true;
                     // If we're moving down, we want the top item. If we're moving up, bottom item.
                     final int motionIndex = delta > 0 ? 0 : getChildCount() - 1;
 
@@ -4518,10 +4518,12 @@
                     final int overscrollMode = getOverScrollMode();
 
                     if (!trackMotionScroll(delta, delta)) {
-                        if (shouldAttemptFling) {
-                            initDifferentialFlingHelperIfNotExists();
-                            mDifferentialMotionFlingHelper.onMotionEvent(event, axis);
+                        if (Flags.platformWidgetHapticScrollFeedback()) {
+                            initHapticScrollFeedbackProviderIfNotExists();
+                            mHapticScrollFeedbackProvider.onScrollProgress(event, axis, delta);
                         }
+                        initDifferentialFlingHelperIfNotExists();
+                        mDifferentialMotionFlingHelper.onMotionEvent(event, axis);
                         return true;
                     } else if (!event.isFromSource(InputDevice.SOURCE_MOUSE) && motionView != null
                             && (overscrollMode == OVER_SCROLL_ALWAYS
@@ -4530,7 +4532,13 @@
                         int motionViewRealTop = motionView.getTop();
                         float overscroll = (delta - (motionViewRealTop - motionViewPrevTop))
                                 / ((float) getHeight());
-                        if (delta > 0) {
+                        boolean hitTopLimit = delta > 0;
+                        if (Flags.platformWidgetHapticScrollFeedback()) {
+                            initHapticScrollFeedbackProviderIfNotExists();
+                            mHapticScrollFeedbackProvider.onScrollLimit(
+                                    event, axis, /* isStart= */ hitTopLimit);
+                        }
+                        if (hitTopLimit) {
                             mEdgeGlowTop.onPullDistance(overscroll, 0.5f);
                             mEdgeGlowTop.onRelease();
                         } else {
@@ -4696,6 +4704,12 @@
         }
     }
 
+    private void initHapticScrollFeedbackProviderIfNotExists() {
+        if (mHapticScrollFeedbackProvider == null) {
+            mHapticScrollFeedbackProvider = new HapticScrollFeedbackProvider(this);
+        }
+    }
+
     private void recycleVelocityTracker() {
         if (mVelocityTracker != null) {
             mVelocityTracker.recycle();
diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java
index d330ebf..90b077b 100644
--- a/core/java/android/widget/ScrollView.java
+++ b/core/java/android/widget/ScrollView.java
@@ -33,6 +33,7 @@
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.FocusFinder;
+import android.view.HapticScrollFeedbackProvider;
 import android.view.InputDevice;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
@@ -47,6 +48,7 @@
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.animation.AnimationUtils;
 import android.view.inspector.InspectableProperty;
+import android.widget.flags.Flags;
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
@@ -206,6 +208,8 @@
 
     private DifferentialMotionFlingHelper mDifferentialMotionFlingHelper;
 
+    private HapticScrollFeedbackProvider mHapticScrollFeedbackProvider;
+
     /**
      * Sentinel value for no current active pointer.
      * Used by {@link #mActivePointerId}.
@@ -604,6 +608,12 @@
         }
     }
 
+    private void initHapticScrollFeedbackProviderIfNotExists() {
+        if (mHapticScrollFeedbackProvider == null) {
+            mHapticScrollFeedbackProvider = new HapticScrollFeedbackProvider(this);
+        }
+    }
+
     private void recycleVelocityTracker() {
         if (mVelocityTracker != null) {
             mVelocityTracker.recycle();
@@ -967,7 +977,7 @@
                     // Tracks whether or not we should attempt fling for this event.
                     // Fling should not be attempted if the view is already at the limit of scroll,
                     // since it conflicts with EdgeEffect.
-                    boolean shouldAttemptFling = true;
+                    boolean hitLimit = false;
                     final int range = getScrollRange();
                     int oldScrollY = mScrollY;
                     int newScrollY = oldScrollY - delta;
@@ -986,7 +996,7 @@
                             absorbed = true;
                         }
                         newScrollY = 0;
-                        shouldAttemptFling = false;
+                        hitLimit = true;
                     } else if (newScrollY > range) {
                         if (canOverscroll) {
                             mEdgeGlowBottom.onPullDistance(
@@ -996,11 +1006,21 @@
                             absorbed = true;
                         }
                         newScrollY = range;
-                        shouldAttemptFling = false;
+                        hitLimit = true;
                     }
                     if (newScrollY != oldScrollY) {
                         super.scrollTo(mScrollX, newScrollY);
-                        if (shouldAttemptFling) {
+                        if (hitLimit) {
+                            if (Flags.platformWidgetHapticScrollFeedback()) {
+                                initHapticScrollFeedbackProviderIfNotExists();
+                                mHapticScrollFeedbackProvider.onScrollLimit(
+                                        event, axis, /* isStart= */ newScrollY == 0);
+                            }
+                        } else {
+                            if (Flags.platformWidgetHapticScrollFeedback()) {
+                                initHapticScrollFeedbackProviderIfNotExists();
+                                mHapticScrollFeedbackProvider.onScrollProgress(event, axis, delta);
+                            }
                             initDifferentialFlingHelperIfNotExists();
                             mDifferentialMotionFlingHelper.onMotionEvent(event, axis);
                         }
diff --git a/core/java/android/widget/flags/scroll_view_flags.aconfig b/core/java/android/widget/flags/scroll_view_flags.aconfig
new file mode 100644
index 0000000..f93ade2
--- /dev/null
+++ b/core/java/android/widget/flags/scroll_view_flags.aconfig
@@ -0,0 +1,8 @@
+package: "android.widget.flags"
+
+flag {
+    namespace: "widget"
+    name: "platform_widget_haptic_scroll_feedback"
+    description: "Enables haptic scroll feedback in platform widgets"
+    bug: "287914819"
+}
\ No newline at end of file
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index c19265a..ad196c0 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -78,6 +78,7 @@
         "liblog",
         "libminikin",
         "libz",
+        "server_configurable_flags",
     ],
 
     static_libs: [
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 367a4f5..7d2690e 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -6698,4 +6698,9 @@
 
     <!-- Whether unlocking and waking a device are sequenced -->
     <bool name="config_orderUnlockAndWake">false</bool>
+
+    <!-- Whether scroll haptic feedback is enabled for rotary encoder scrolls on
+         {@link MotionEvent#AXIS_SCROLL} generated by {@link InputDevice#SOURCE_ROTARY_ENCODER}
+         devices. -->
+    <bool name="config_viewRotaryEncoderHapticScrollFedbackEnabled">false</bool>
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 1965172..80c2fbf 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -5230,4 +5230,5 @@
   <java-symbol type="bool" name="config_tvExternalInputLoggingDisplayNameFilterEnabled" />
   <java-symbol type="array" name="config_tvExternalInputLoggingDeviceOnScreenDisplayNames" />
   <java-symbol type="array" name="config_tvExternalInputLoggingDeviceBrandNames" />
+  <java-symbol type="bool" name="config_viewRotaryEncoderHapticScrollFedbackEnabled" />
 </resources>
diff --git a/core/tests/coretests/src/android/hardware/input/InputFlagsTest.java b/core/tests/coretests/src/android/hardware/input/InputFlagsTest.java
new file mode 100644
index 0000000..5aeab42
--- /dev/null
+++ b/core/tests/coretests/src/android/hardware/input/InputFlagsTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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 android.hardware.input;
+
+import static com.android.hardware.input.Flags.keyboardA11yStickyKeysFlag;
+import static com.android.hardware.input.Flags.keyboardLayoutPreviewFlag;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for {@link com.android.hardware.input.Flags}
+ *
+ * Build/Install/Run:
+ *  atest FrameworksCoreTests:InputFlagsTest
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class InputFlagsTest {
+
+    /**
+     * Test that the flags work
+     */
+    @Test
+    public void testFlags() {
+        // No crash when accessing the flag.
+        keyboardLayoutPreviewFlag();
+        keyboardA11yStickyKeysFlag();
+    }
+}
+
diff --git a/core/tests/coretests/src/android/view/HapticScrollFeedbackProviderTest.java b/core/tests/coretests/src/android/view/HapticScrollFeedbackProviderTest.java
index 6bdb07d..a2c41e4 100644
--- a/core/tests/coretests/src/android/view/HapticScrollFeedbackProviderTest.java
+++ b/core/tests/coretests/src/android/view/HapticScrollFeedbackProviderTest.java
@@ -19,8 +19,10 @@
 import static android.view.HapticFeedbackConstants.SCROLL_ITEM_FOCUS;
 import static android.view.HapticFeedbackConstants.SCROLL_LIMIT;
 import static android.view.HapticFeedbackConstants.SCROLL_TICK;
-
 import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import android.content.Context;
 import android.platform.test.annotations.Presubmit;
@@ -32,6 +34,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -43,28 +46,71 @@
     private static final int INPUT_DEVICE_1 = 1;
     private static final int INPUT_DEVICE_2 = 2;
 
-    private static final int TICK_INTERVAL_PIXELS = 100;
-
     private TestView mView;
     private long mCurrentTimeMillis = 1000; // arbitrary starting time value
 
+    @Mock ViewConfiguration mMockViewConfig;
+
     private HapticScrollFeedbackProvider mProvider;
 
     @Before
     public void setUp() {
+        mMockViewConfig = mock(ViewConfiguration.class);
+        setHapticScrollFeedbackEnabled(true);
+
         mView = new TestView(InstrumentationRegistry.getContext());
-        mProvider = new HapticScrollFeedbackProvider(mView, TICK_INTERVAL_PIXELS);
+        mProvider = new HapticScrollFeedbackProvider(mView, mMockViewConfig);
     }
 
     @Test
-    public void testSnapToItem() {
+    public void testNoFeedbackWhenFeedbackIsDisabled() {
+        setHapticScrollFeedbackEnabled(false);
+        // Call different types scroll feedback methods; non of them should produce feedback because
+        // feedback has been disabled.
+        mProvider.onSnapToItem(createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL);
+        mProvider.onSnapToItem(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL);
+        mProvider.onScrollLimit(
+                createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ true);
+        mProvider.onScrollLimit(
+                createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false);
+        mProvider.onScrollLimit(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* isStart= */ true);
+        mProvider.onScrollLimit(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* isStart= */ false);
+        mProvider.onScrollProgress(
+                createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 10);
+        mProvider.onScrollProgress(
+                createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ -9);
+        mProvider.onScrollProgress(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* deltaInPixels= */ 300);
+        mProvider.onScrollProgress(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* deltaInPixels= */ -300);
+
+        assertNoFeedback(mView);
+    }
+
+    @Test
+    public void testSnapToItem_withMotionEvent() {
         mProvider.onSnapToItem(createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL);
 
         assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_ITEM_FOCUS);
     }
 
     @Test
-    public void testScrollLimit_start() {
+    public void testSnapToItem_withDeviceIdAndSource() {
+        mProvider.onSnapToItem(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL);
+
+        assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_ITEM_FOCUS);
+    }
+
+    @Test
+    public void testScrollLimit_start_withMotionEvent() {
         mProvider.onScrollLimit(
                 createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ true);
 
@@ -72,7 +118,16 @@
     }
 
     @Test
-    public void testScrollLimit_stop() {
+    public void testScrollLimit_start_withDeviceIdAndSource() {
+        mProvider.onScrollLimit(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* isStart= */ true);
+
+        assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT);
+    }
+
+    @Test
+    public void testScrollLimit_stop_withMotionEvent() {
         mProvider.onScrollLimit(
                 createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false);
 
@@ -80,10 +135,17 @@
     }
 
     @Test
-    public void testScrollProgress_zeroTickInterval() {
-        mProvider =
-                new HapticScrollFeedbackProvider(
-                        mView, /* rotaryEncoderAxisScrollTickIntervalPixels= */ 0);
+    public void testScrollLimit_stop_withDeviceIdAndSource() {
+        mProvider.onScrollLimit(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* isStart= */ false);
+
+        assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT);
+    }
+
+    @Test
+    public void testScrollProgress_zeroTickInterval_withMotionEvent() {
+        setHapticScrollTickInterval(0);
 
         mProvider.onScrollProgress(
                 createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 10);
@@ -96,10 +158,22 @@
     }
 
     @Test
-    public void testScrollProgress_progressEqualsOrExceedsPositiveThreshold() {
-        mProvider =
-                new HapticScrollFeedbackProvider(
-                        mView, /* rotaryEncoderAxisScrollTickIntervalPixels= */ 100);
+    public void testScrollProgress_zeroTickInterval_withDeviceIdAndSource() {
+        setHapticScrollTickInterval(0);
+
+        mProvider.onScrollProgress(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* deltaInPixels= */ 30);
+        mProvider.onScrollProgress(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* deltaInPixels= */ 20);
+
+        assertNoFeedback(mView);
+    }
+
+    @Test
+    public void testScrollProgress_progressEqualsOrExceedsPositiveThreshold_withMotionEvent() {
+        setHapticScrollTickInterval(100);
 
         mProvider.onScrollProgress(
                 createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 20);
@@ -121,10 +195,30 @@
     }
 
     @Test
-    public void testScrollProgress_progressEqualsOrExceedsNegativeThreshold() {
-        mProvider =
-                new HapticScrollFeedbackProvider(
-                        mView, /* rotaryEncoderAxisScrollTickIntervalPixels= */ 100);
+    public void testScrollProgress_progressEqualsOrExceedsPositiveThreshold_withDeviceIdAndSrc() {
+        setHapticScrollTickInterval(100);
+        mProvider.onScrollProgress(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* deltaInPixels= */ 20);
+
+        assertNoFeedback(mView);
+
+        mProvider.onScrollProgress(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* deltaInPixels= */ 80);
+
+        assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_TICK, 1);
+
+        mProvider.onScrollProgress(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* deltaInPixels= */ 120);
+
+        assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_TICK, 2);
+    }
+
+    @Test
+    public void testScrollProgress_progressEqualsOrExceedsNegativeThreshold_withMotionEvent() {
+        setHapticScrollTickInterval(100);
 
         mProvider.onScrollProgress(
                 createRotaryEncoderScrollEvent(),
@@ -153,10 +247,34 @@
     }
 
     @Test
-    public void testScrollProgress_positiveAndNegativeProgresses() {
-        mProvider =
-                new HapticScrollFeedbackProvider(
-                        mView, /* rotaryEncoderAxisScrollTickIntervalPixels= */ 100);
+    public void testScrollProgress_progressEqualsOrExceedsNegativeThreshold_withDeviceIdAndSrc() {
+        setHapticScrollTickInterval(100);
+
+        mProvider.onScrollProgress(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* deltaInPixels= */ -20);
+
+        assertNoFeedback(mView);
+
+        mProvider.onScrollProgress(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* deltaInPixels= */ -80);
+
+        assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_TICK, 1);
+
+        mProvider.onScrollProgress(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* deltaInPixels= */ -70);
+        mProvider.onScrollProgress(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* deltaInPixels= */ -40);
+
+        assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_TICK, 2);
+    }
+
+    @Test
+    public void testScrollProgress_positiveAndNegativeProgresses_withMotionEvent() {
+        setHapticScrollTickInterval(100);
 
         mProvider.onScrollProgress(
                 createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 20);
@@ -190,10 +308,48 @@
     }
 
     @Test
-    public void testScrollProgress_singleProgressExceedsThreshold() {
-        mProvider =
-                new HapticScrollFeedbackProvider(
-                        mView, /* rotaryEncoderAxisScrollTickIntervalPixels= */ 100);
+    public void testScrollProgress_positiveAndNegativeProgresses_withDeviceIdAndSource() {
+        setHapticScrollTickInterval(100);
+
+        mProvider.onScrollProgress(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* deltaInPixels= */ 20);
+        mProvider.onScrollProgress(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* deltaInPixels= */ -90);
+
+        assertNoFeedback(mView);
+
+        mProvider.onScrollProgress(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* deltaInPixels= */ 10);
+
+        assertNoFeedback(mView);
+
+        mProvider.onScrollProgress(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* deltaInPixels= */ -50);
+
+        assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_TICK, 1);
+
+        mProvider.onScrollProgress(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* deltaInPixels= */ 40);
+        mProvider.onScrollProgress(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* deltaInPixels= */ 50);
+        mProvider.onScrollProgress(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* deltaInPixels= */ 60);
+
+
+
+        assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_TICK, 2);
+    }
+
+    @Test
+    public void testScrollProgress_singleProgressExceedsThreshold_withMotionEvent() {
+        setHapticScrollTickInterval(100);
 
         mProvider.onScrollProgress(
                 createRotaryEncoderScrollEvent(),
@@ -204,7 +360,18 @@
     }
 
     @Test
-    public void testScrollLimit_startAndEndLimit_playsOnlyOneFeedback() {
+    public void testScrollProgress_singleProgressExceedsThreshold_withDeviceIdAndSource() {
+        setHapticScrollTickInterval(100);
+
+        mProvider.onScrollProgress(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* deltaInPixels= */ 1000);
+
+        assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_TICK, 1);
+    }
+
+    @Test
+    public void testScrollLimit_startAndEndLimit_playsOnlyOneFeedback_withMotionEvent() {
         mProvider.onScrollLimit(
                 createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false);
         mProvider.onScrollLimit(
@@ -214,7 +381,19 @@
     }
 
     @Test
-    public void testScrollLimit_doubleStartLimit_playsOnlyOneFeedback() {
+    public void testScrollLimit_startAndEndLimit_playsOnlyOneFeedback_withDeviceIdAndSource() {
+        mProvider.onScrollLimit(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* isStart= */ false);
+        mProvider.onScrollLimit(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* isStart= */ true);
+
+        assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT);
+    }
+
+    @Test
+    public void testScrollLimit_doubleStartLimit_playsOnlyOneFeedback_withMotionEvent() {
         mProvider.onScrollLimit(
                 createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ true);
         mProvider.onScrollLimit(
@@ -224,7 +403,19 @@
     }
 
     @Test
-    public void testScrollLimit_doubleEndLimit_playsOnlyOneFeedback() {
+    public void testScrollLimit_doubleStartLimit_playsOnlyOneFeedback_withDeviceIdAndSource() {
+        mProvider.onScrollLimit(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* isStart= */ true);
+        mProvider.onScrollLimit(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* isStart= */ true);
+
+        assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT);
+    }
+
+    @Test
+    public void testScrollLimit_doubleEndLimit_playsOnlyOneFeedback_withMotionEvent() {
         mProvider.onScrollLimit(
                 createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false);
         mProvider.onScrollLimit(
@@ -234,7 +425,37 @@
     }
 
     @Test
-    public void testScrollLimit_enabledWithProgress() {
+    public void testScrollLimit_doubleEndLimit_playsOnlyOneFeedback_withDeviceIdAndSource() {
+        mProvider.onScrollLimit(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* isStart= */ false);
+        mProvider.onScrollLimit(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* isStart= */ false);
+
+        assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT);
+    }
+
+    @Test
+    public void testScrollLimit_notEnabledWithZeroProgress() {
+        mProvider.onScrollLimit(
+                createRotaryEncoderScrollEvent(INPUT_DEVICE_1), MotionEvent.AXIS_SCROLL,
+                /* isStart= */ false);
+
+        mProvider.onScrollProgress(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* deltaInPixels= */ 0);
+        mProvider.onScrollLimit(
+                createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ true);
+        mProvider.onScrollLimit(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* isStart= */ false);
+
+        assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT, 1);
+    }
+
+    @Test
+    public void testScrollLimit_enabledWithProgress_withMotionEvent() {
         mProvider.onScrollLimit(
                 createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false);
 
@@ -247,7 +468,23 @@
     }
 
     @Test
-    public void testScrollLimit_enabledWithSnap() {
+    public void testScrollLimit_enabledWithProgress_withDeviceIdAndSource() {
+        mProvider.onScrollLimit(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* isStart= */ false);
+
+        mProvider.onScrollProgress(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* deltaInPixels= */ 80);
+        mProvider.onScrollLimit(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* isStart= */ false);
+
+        assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT, 2);
+    }
+
+    @Test
+    public void testScrollLimit_enabledWithSnap_withMotionEvent() {
         mProvider.onScrollLimit(
                 createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false);
 
@@ -259,7 +496,22 @@
     }
 
     @Test
-    public void testScrollLimit_enabledWithDissimilarSnap() {
+    public void testScrollLimit_enabledWithSnap_withDeviceIdAndSource() {
+        mProvider.onScrollLimit(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* isStart= */ false);
+
+        mProvider.onSnapToItem(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL);
+        mProvider.onScrollLimit(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* isStart= */ false);
+
+        assertFeedbackCount(mView, HapticFeedbackConstants.SCROLL_LIMIT, 2);
+    }
+
+    @Test
+    public void testScrollLimit_enabledWithDissimilarSnap_withMotionEvent() {
         mProvider.onScrollLimit(
                 createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false);
 
@@ -271,7 +523,22 @@
     }
 
     @Test
-    public void testScrollLimit_enabledWithDissimilarProgress() {
+    public void testScrollLimit_enabledWithDissimilarSnap_withDeviceIdAndSource() {
+        mProvider.onScrollLimit(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* isStart= */ false);
+
+        mProvider.onSnapToItem(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_X);
+        mProvider.onScrollLimit(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* isStart= */ false);
+
+        assertFeedbackCount(mView, HapticFeedbackConstants.SCROLL_LIMIT, 2);
+    }
+
+    @Test
+    public void testScrollLimit_enabledWithDissimilarProgress_withMotionEvent() {
         mProvider.onScrollLimit(
                 createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false);
 
@@ -284,7 +551,23 @@
     }
 
     @Test
-    public void testScrollLimit_enabledWithDissimilarLimit() {
+    public void testScrollLimit_enabledWithDissimilarProgress_withDeviceIdAndSource() {
+        mProvider.onScrollLimit(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* isStart= */ false);
+
+        mProvider.onScrollProgress(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* deltaInPixels= */ 80);
+        mProvider.onScrollLimit(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* isStart= */ false);
+
+        assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT, 2);
+    }
+
+    @Test
+    public void testScrollLimit_enabledWithDissimilarLimit_withMotionEvent() {
         mProvider.onScrollLimit(
                 createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* isStart= */ false);
 
@@ -296,7 +579,22 @@
     }
 
     @Test
-    public void testScrollLimit_enabledWithMotionFromDifferentDeviceId() {
+    public void testScrollLimit_enabledWithDissimilarLimit_withDeviceIdAndSource() {
+        mProvider.onScrollLimit(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* isStart= */ false);
+
+        mProvider.onScrollLimit(INPUT_DEVICE_2, InputDevice.SOURCE_TOUCHSCREEN, MotionEvent.AXIS_X,
+                /* isStart= */ false);
+        mProvider.onScrollLimit(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* isStart= */ false);
+
+        assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT, 3);
+    }
+
+    @Test
+    public void testScrollLimit_enabledWithMotionFromDifferentDeviceId_withMotionEvent() {
         mProvider.onScrollLimit(
                 createRotaryEncoderScrollEvent(INPUT_DEVICE_1),
                 MotionEvent.AXIS_SCROLL,
@@ -314,6 +612,78 @@
         assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT, 3);
     }
 
+    @Test
+    public void testScrollLimit_enabledWithMotionFromDifferentDeviceId_withDeviceIdAndSource() {
+        mProvider.onScrollLimit(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* isStart= */ false);
+
+        mProvider.onScrollLimit(
+                INPUT_DEVICE_2,
+                InputDevice.SOURCE_ROTARY_ENCODER,
+                MotionEvent.AXIS_SCROLL,
+                /* isStart= */ false);
+        mProvider.onScrollLimit(
+                INPUT_DEVICE_1,
+                InputDevice.SOURCE_ROTARY_ENCODER,
+                MotionEvent.AXIS_SCROLL,
+                /* isStart= */ false);
+
+        assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT, 3);
+    }
+
+    @Test
+    public void testSnapToItem_differentApis() {
+        mProvider.onSnapToItem(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL);
+        mProvider.onSnapToItem(createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL);
+
+        assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_ITEM_FOCUS, 2);
+    }
+
+    @Test
+    public void testScrollLimit_differentApis() {
+        mProvider.onScrollLimit(
+                createRotaryEncoderScrollEvent(INPUT_DEVICE_1),
+                MotionEvent.AXIS_SCROLL,
+                /* isStart= */ false);
+        mProvider.onScrollLimit(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* isStart= */ false);
+
+        assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT, 1);
+
+        mProvider.onScrollLimit(
+                INPUT_DEVICE_2, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* isStart= */ true);
+        mProvider.onScrollLimit(
+                createRotaryEncoderScrollEvent(INPUT_DEVICE_2),
+                MotionEvent.AXIS_SCROLL,
+                /* isStart= */ true);
+
+        assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_LIMIT, 2);
+    }
+
+    @Test
+    public void testScrollProgress_differentApis() {
+        setHapticScrollTickInterval(100);
+
+        // Neither types of APIs independently excceeds the "100" tick interval.
+        // But the combined deltas pass 100.
+        mProvider.onScrollProgress(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* deltaInPixels= */ 20);
+        mProvider.onScrollProgress(
+                createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 40);
+        mProvider.onScrollProgress(
+                INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL,
+                /* deltaInPixels= */ 30);
+        mProvider.onScrollProgress(
+                createRotaryEncoderScrollEvent(), MotionEvent.AXIS_SCROLL, /* deltaInPixels= */ 30);
+
+        assertOnlyFeedback(mView, HapticFeedbackConstants.SCROLL_TICK, 1);
+    }
+
     private void assertNoFeedback(TestView view) {
         for (int feedback : new int[] {SCROLL_ITEM_FOCUS, SCROLL_LIMIT, SCROLL_TICK}) {
             assertFeedbackCount(view, feedback, 0);
@@ -335,6 +705,16 @@
         assertThat(count).isEqualTo(expectedCount);
     }
 
+    private void setHapticScrollTickInterval(int interval) {
+        when(mMockViewConfig.getHapticScrollFeedbackTickInterval(anyInt(), anyInt(), anyInt()))
+                .thenReturn(interval);
+    }
+
+    private void setHapticScrollFeedbackEnabled(boolean enabled) {
+        when(mMockViewConfig.isHapticScrollFeedbackEnabled(anyInt(), anyInt(), anyInt()))
+                .thenReturn(enabled);
+    }
+
     private MotionEvent createTouchMoveEvent() {
         long downTime = mCurrentTimeMillis;
         long eventTime = mCurrentTimeMillis + 2; // arbitrary increment from the down time.
@@ -386,4 +766,4 @@
             return true;
         }
     }
-}
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/AndroidTestTemplate.xml
index 5264cae..57df4e8 100644
--- a/libs/WindowManager/Shell/tests/flicker/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/AndroidTestTemplate.xml
@@ -104,7 +104,7 @@
         <option name="directory-keys"
                 value="/data/user/0/com.android.wm.shell.flicker.splitscreen/files"/>
         <option name="directory-keys"
-            value="/data/user/0/com.android.server.wm.flicker.service/files"/>
+                value="/data/user/0/com.android.wm.shell.flicker.service/files"/>
         <option name="collect-on-run-ended-only" value="true"/>
         <option name="clean-up" value="true"/>
     </metrics_collector>
diff --git a/libs/hwui/pipeline/skia/ReorderBarrierDrawables.cpp b/libs/hwui/pipeline/skia/ReorderBarrierDrawables.cpp
index 11977bd..136740c 100644
--- a/libs/hwui/pipeline/skia/ReorderBarrierDrawables.cpp
+++ b/libs/hwui/pipeline/skia/ReorderBarrierDrawables.cpp
@@ -27,7 +27,6 @@
 #include <SkRect.h>
 #include <SkScalar.h>
 #include <SkShadowUtils.h>
-#include <include/private/SkShadowFlags.h>
 
 namespace android {
 namespace uirenderer {
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
index 11a8956..4a252a9 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
@@ -23,10 +23,14 @@
 import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider
 import com.android.settingslib.spa.gallery.dialog.AlertDialogPageProvider
+import com.android.settingslib.spa.gallery.editor.EditorMainPageProvider
+import com.android.settingslib.spa.gallery.editor.SettingsExposedDropdownMenuBoxPageProvider
+import com.android.settingslib.spa.gallery.editor.SettingsExposedDropdownMenuCheckBoxProvider
 import com.android.settingslib.spa.gallery.home.HomePageProvider
 import com.android.settingslib.spa.gallery.itemList.ItemListPageProvider
 import com.android.settingslib.spa.gallery.itemList.ItemOperatePageProvider
 import com.android.settingslib.spa.gallery.itemList.OperateListPageProvider
+import com.android.settingslib.spa.gallery.editor.SettingsOutlinedTextFieldPageProvider
 import com.android.settingslib.spa.gallery.page.ArgumentPageProvider
 import com.android.settingslib.spa.gallery.page.ChartPageProvider
 import com.android.settingslib.spa.gallery.page.FooterPageProvider
@@ -84,6 +88,10 @@
                 ItemListPageProvider,
                 ItemOperatePageProvider,
                 OperateListPageProvider,
+                EditorMainPageProvider,
+                SettingsOutlinedTextFieldPageProvider,
+                SettingsExposedDropdownMenuBoxPageProvider,
+                SettingsExposedDropdownMenuCheckBoxProvider,
             ),
             rootPages = listOf(
                 HomePageProvider.createSettingsPage(),
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt
new file mode 100644
index 0000000..b74af21
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.settingslib.spa.gallery.editor
+
+import android.os.Bundle
+import com.android.settingslib.spa.framework.common.SettingsEntry
+import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
+import com.android.settingslib.spa.framework.compose.navigator
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+
+private const val TITLE = "Category: Editor"
+
+object EditorMainPageProvider : SettingsPageProvider {
+    override val name = "EditorMain"
+    private val owner = createSettingsPage()
+
+    override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
+        return listOf(
+            SettingsOutlinedTextFieldPageProvider.buildInjectEntry().setLink(fromPage = owner)
+                .build(),
+            SettingsExposedDropdownMenuBoxPageProvider.buildInjectEntry().setLink(fromPage = owner)
+                .build(),
+            SettingsExposedDropdownMenuCheckBoxProvider.buildInjectEntry().setLink(fromPage = owner)
+                .build(),
+        )
+    }
+
+    fun buildInjectEntry(): SettingsEntryBuilder {
+        return SettingsEntryBuilder.createInject(owner = owner)
+            .setUiLayoutFn {
+                Preference(object : PreferenceModel {
+                    override val title = TITLE
+                    override val onClick = navigator(name)
+                })
+            }
+    }
+
+    override fun getTitle(arguments: Bundle?): String {
+        return TITLE
+    }
+}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsExposedDropdownMenuBoxPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsExposedDropdownMenuBoxPageProvider.kt
new file mode 100644
index 0000000..6d22e6a
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsExposedDropdownMenuBoxPageProvider.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.settingslib.spa.gallery.editor
+
+import android.os.Bundle
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.tooling.preview.Preview
+import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
+import com.android.settingslib.spa.framework.compose.navigator
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.widget.editor.SettingsExposedDropdownMenuBox
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+
+private const val TITLE = "Sample SettingsExposedDropdownMenuBox"
+
+object SettingsExposedDropdownMenuBoxPageProvider : SettingsPageProvider {
+    override val name = "SettingsExposedDropdownMenuBox"
+    private const val exposedDropdownMenuBoxLabel = "ExposedDropdownMenuBoxLabel"
+
+    override fun getTitle(arguments: Bundle?): String {
+        return TITLE
+    }
+
+    @Composable
+    override fun Page(arguments: Bundle?) {
+        var selectedItem by remember { mutableStateOf("item1") }
+        val options = listOf("item1", "item2", "item3")
+        RegularScaffold(title = TITLE) {
+            SettingsExposedDropdownMenuBox(
+                label = exposedDropdownMenuBoxLabel,
+                options = options,
+                selectedOptionText = selectedItem,
+                enabled = true,
+                onselectedOptionTextChange = { selectedItem = it })
+        }
+    }
+
+    fun buildInjectEntry(): SettingsEntryBuilder {
+        return SettingsEntryBuilder.createInject(owner = createSettingsPage())
+            .setUiLayoutFn {
+                Preference(object : PreferenceModel {
+                    override val title = TITLE
+                    override val onClick = navigator(name)
+                })
+            }
+    }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun SettingsExposedDropdownMenuBoxPagePreview() {
+    SettingsTheme {
+        SettingsExposedDropdownMenuBoxPageProvider.Page(null)
+    }
+}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsExposedDropdownMenuCheckBoxProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsExposedDropdownMenuCheckBoxProvider.kt
new file mode 100644
index 0000000..292e002
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsExposedDropdownMenuCheckBoxProvider.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.settingslib.spa.gallery.editor
+
+import android.os.Bundle
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.tooling.preview.Preview
+import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
+import com.android.settingslib.spa.framework.compose.navigator
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.widget.editor.SettingsExposedDropdownMenuCheckBox
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+
+private const val TITLE = "Sample SettingsExposedDropdownMenuCheckBox"
+
+object SettingsExposedDropdownMenuCheckBoxProvider : SettingsPageProvider {
+    override val name = "SettingsExposedDropdownMenuCheckBox"
+    private const val exposedDropdownMenuCheckBoxLabel = "ExposedDropdownMenuCheckBoxLabel"
+    private val options = listOf("item1", "item2", "item3")
+    private val selectedOptionsState1 = mutableStateListOf("item1", "item2")
+
+    override fun getTitle(arguments: Bundle?): String {
+        return TITLE
+    }
+
+    @Composable
+    override fun Page(arguments: Bundle?) {
+        RegularScaffold(title = TITLE) {
+            SettingsExposedDropdownMenuCheckBox(label = exposedDropdownMenuCheckBoxLabel,
+                options = options,
+                selectedOptionsState = remember { selectedOptionsState1 },
+                enabled = true,
+                onselectedOptionStateChange = {})
+        }
+    }
+
+    fun buildInjectEntry(): SettingsEntryBuilder {
+        return SettingsEntryBuilder.createInject(owner = createSettingsPage()).setUiLayoutFn {
+                Preference(object : PreferenceModel {
+                    override val title = TITLE
+                    override val onClick = navigator(name)
+                })
+            }
+    }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun SettingsExposedDropdownMenuCheckBoxPagePreview() {
+    SettingsTheme {
+        SettingsExposedDropdownMenuCheckBoxProvider.Page(null)
+    }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsOutlinedTextFieldPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsOutlinedTextFieldPageProvider.kt
new file mode 100644
index 0000000..54fa5a9
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsOutlinedTextFieldPageProvider.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.settingslib.spa.gallery.editor
+
+import android.os.Bundle
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.tooling.preview.Preview
+import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
+import com.android.settingslib.spa.framework.compose.navigator
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.widget.editor.SettingsOutlinedTextField
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+
+private const val TITLE = "Sample SettingsOutlinedTextField"
+
+object SettingsOutlinedTextFieldPageProvider : SettingsPageProvider {
+    override val name = "SettingsOutlinedTextField"
+
+    override fun getTitle(arguments: Bundle?): String {
+        return TITLE
+    }
+
+    @Composable
+    override fun Page(arguments: Bundle?) {
+        var value by remember { mutableStateOf("Enabled Value") }
+        RegularScaffold(title = TITLE) {
+            SettingsOutlinedTextField(
+                value = value,
+                label = "OutlinedTextField Enabled",
+                enabled = true,
+                onTextChange = {value = it})
+        }
+    }
+
+    fun buildInjectEntry(): SettingsEntryBuilder {
+        return SettingsEntryBuilder.createInject(owner = createSettingsPage())
+            .setUiLayoutFn {
+                Preference(object : PreferenceModel {
+                    override val title = TITLE
+                    override val onClick = navigator(name)
+                })
+            }
+    }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun SettingsOutlinedTextFieldPagePreview() {
+    SettingsTheme {
+        SettingsOutlinedTextFieldPageProvider.Page(null)
+    }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt
similarity index 96%
rename from packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt
rename to packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt
index c60ebfd..bb311a5 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt
@@ -29,6 +29,7 @@
 import com.android.settingslib.spa.gallery.SettingsPageProviderEnum
 import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider
 import com.android.settingslib.spa.gallery.dialog.AlertDialogPageProvider
+import com.android.settingslib.spa.gallery.editor.EditorMainPageProvider
 import com.android.settingslib.spa.gallery.itemList.OperateListPageProvider
 import com.android.settingslib.spa.gallery.page.ArgumentPageModel
 import com.android.settingslib.spa.gallery.page.ArgumentPageProvider
@@ -65,6 +66,7 @@
             LoadingBarPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
             ChartPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
             AlertDialogPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
+            EditorMainPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
         )
     }
 
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBox.kt
new file mode 100644
index 0000000..d7bbf08
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBox.kt
@@ -0,0 +1,107 @@
+/*
+ * 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.settingslib.spa.widget.editor
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.tooling.preview.Preview
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+fun SettingsExposedDropdownMenuBox(
+    label: String,
+    options: List<String>,
+    selectedOptionText: String,
+    enabled: Boolean,
+    onselectedOptionTextChange: (String) -> Unit,
+) {
+    var expanded by remember { mutableStateOf(false) }
+    ExposedDropdownMenuBox(
+        expanded = expanded,
+        onExpandedChange = { expanded = it },
+        modifier = Modifier
+            .padding(SettingsDimension.itemPadding),
+    ) {
+        OutlinedTextField(
+            // The `menuAnchor` modifier must be passed to the text field for correctness.
+            modifier = Modifier
+                .menuAnchor()
+                .fillMaxWidth(),
+            value = selectedOptionText,
+            onValueChange = onselectedOptionTextChange,
+            label = { Text(text = label) },
+            trailingIcon = {
+                ExposedDropdownMenuDefaults.TrailingIcon(
+                    expanded = expanded
+                )
+            },
+            singleLine = true,
+            readOnly = true,
+            enabled = enabled
+        )
+        if (options.isNotEmpty()) {
+            ExposedDropdownMenu(
+                expanded = expanded,
+                modifier = Modifier
+                    .fillMaxWidth(),
+                onDismissRequest = { expanded = false },
+            ) {
+                options.forEach { option ->
+                    DropdownMenuItem(
+                        text = { Text(option) },
+                        onClick = {
+                            onselectedOptionTextChange(option)
+                            expanded = false
+                        },
+                        contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
+                    )
+                }
+            }
+        }
+    }
+}
+
+@Preview
+@Composable
+private fun SettingsExposedDropdownMenuBoxsPreview() {
+    val item1 = "item1"
+    val item2 = "item2"
+    val item3 = "item3"
+    val options = listOf(item1, item2, item3)
+    SettingsTheme {
+        SettingsExposedDropdownMenuBox(
+            label = "ExposedDropdownMenuBoxLabel",
+            options = options,
+            selectedOptionText = item1,
+            enabled = true,
+            onselectedOptionTextChange = {})
+    }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt
new file mode 100644
index 0000000..6b3ae77
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt
@@ -0,0 +1,131 @@
+/*
+ * 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.settingslib.spa.widget.editor
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.SnapshotStateList
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SettingsExposedDropdownMenuCheckBox(
+    label: String,
+    options: List<String>,
+    selectedOptionsState: SnapshotStateList<String>,
+    enabled: Boolean,
+    onselectedOptionStateChange: (String) -> Unit,
+) {
+    var dropDownWidth by remember { mutableStateOf(0) }
+    var expanded by remember { mutableStateOf(false) }
+    ExposedDropdownMenuBox(
+        expanded = expanded,
+        onExpandedChange = { expanded = it },
+        modifier = Modifier.width(350.dp).padding(
+            horizontal = SettingsDimension.itemPaddingEnd,
+            vertical = SettingsDimension.itemPaddingVertical
+        ).onSizeChanged {
+            dropDownWidth = it.width
+        },
+    ) {
+        OutlinedTextField(
+            // The `menuAnchor` modifier must be passed to the text field for correctness.
+            modifier = Modifier.menuAnchor().fillMaxWidth(),
+            value = selectedOptionsState.joinToString(", "),
+            onValueChange = onselectedOptionStateChange,
+            label = { Text(text = label) },
+            trailingIcon = {
+                ExposedDropdownMenuDefaults.TrailingIcon(
+                    expanded = expanded
+                )
+            },
+            readOnly = true,
+            enabled = enabled
+        )
+        if (options.isNotEmpty()) {
+            ExposedDropdownMenu(
+                expanded = expanded,
+                modifier = Modifier.fillMaxWidth()
+                    .width(with(LocalDensity.current) { dropDownWidth.toDp() }),
+                onDismissRequest = { expanded = false },
+            ) {
+                options.forEach { option ->
+                    TextButton(modifier = Modifier.fillMaxHeight().fillMaxWidth(), onClick = {
+                        if (selectedOptionsState.contains(option)) {
+                            selectedOptionsState.remove(
+                                option
+                            )
+                        } else {
+                            selectedOptionsState.add(
+                                option
+                            )
+                        }
+                    }) {
+                        Row(
+                            modifier = Modifier.fillMaxHeight().fillMaxWidth(),
+                            horizontalArrangement = Arrangement.Start,
+                            verticalAlignment = Alignment.CenterVertically
+                        ) {
+                            Checkbox(checked = selectedOptionsState.contains(
+                                option
+                            ), onCheckedChange = {})
+                            Text(text = option)
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Preview
+@Composable
+private fun ActionButtonsPreview() {
+    val options = listOf("item1", "item2", "item3")
+    val selectedOptionsState = remember { mutableStateListOf("item1", "item2") }
+    SettingsTheme {
+        SettingsExposedDropdownMenuCheckBox(label = "label",
+            options = options,
+            selectedOptionsState = selectedOptionsState,
+            enabled = true,
+            onselectedOptionStateChange = {})
+    }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsOutlinedTextField.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsOutlinedTextField.kt
new file mode 100644
index 0000000..3819a10
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsOutlinedTextField.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.settingslib.spa.widget.editor
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+
+@Composable
+fun SettingsOutlinedTextField(
+    value: String,
+    label: String,
+    singleLine: Boolean = true,
+    enabled: Boolean = true,
+    onTextChange: (String) -> Unit,
+) {
+    OutlinedTextField(
+        modifier = Modifier
+            .fillMaxWidth()
+            .padding(SettingsDimension.itemPadding),
+        value = value,
+        onValueChange = onTextChange,
+        label = {
+            Text(text = label)
+        },
+        singleLine = singleLine,
+        enabled = enabled,
+    )
+}
+
+@Preview
+@Composable
+private fun SettingsOutlinedTextFieldPreview() {
+    var value by remember { mutableStateOf("Enabled Value") }
+    SettingsTheme {
+        SettingsOutlinedTextField(
+            value = value,
+            label = "OutlinedTextField Enabled",
+            enabled = true,
+            onTextChange = {value = it})
+    }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBoxTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBoxTest.kt
new file mode 100644
index 0000000..09f5945
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBoxTest.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.settingslib.spa.widget.editor
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SettingsExposedDropdownMenuBoxTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+    private val options = listOf("item1", "item2", "item3")
+    private val item2 = "item2"
+    private val exposedDropdownMenuBoxLabel = "ExposedDropdownMenuBoxLabel"
+
+    @Test
+    fun exposedDropdownMenuBoxs_displayed() {
+        composeTestRule.setContent {
+            var selectedItem by remember { mutableStateOf("item1") }
+            SettingsExposedDropdownMenuBox(
+                label = exposedDropdownMenuBoxLabel,
+                options = options,
+                selectedOptionText = selectedItem,
+                enabled = true,
+                onselectedOptionTextChange = {selectedItem = it})
+        }
+        composeTestRule.onNodeWithText(exposedDropdownMenuBoxLabel, substring = true)
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun exposedDropdownMenuBoxs_expanded() {
+        composeTestRule.setContent {
+            var selectedItem by remember { mutableStateOf("item1") }
+            SettingsExposedDropdownMenuBox(
+                label = exposedDropdownMenuBoxLabel,
+                options = options,
+                selectedOptionText = selectedItem,
+                enabled = true,
+                onselectedOptionTextChange = {selectedItem = it})
+        }
+        composeTestRule.onNodeWithText(item2, substring = true)
+            .assertDoesNotExist()
+        composeTestRule.onNodeWithText(exposedDropdownMenuBoxLabel, substring = true)
+            .performClick()
+        composeTestRule.onNodeWithText(item2, substring = true)
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun exposedDropdownMenuBoxs_valueChanged() {
+        composeTestRule.setContent {
+            var selectedItem by remember { mutableStateOf("item1") }
+            SettingsExposedDropdownMenuBox(
+                label = exposedDropdownMenuBoxLabel,
+                options = options,
+                selectedOptionText = selectedItem,
+                enabled = true,
+                onselectedOptionTextChange = {selectedItem = it})
+        }
+        composeTestRule.onNodeWithText(item2, substring = true)
+            .assertDoesNotExist()
+        composeTestRule.onNodeWithText(exposedDropdownMenuBoxLabel, substring = true)
+            .performClick()
+        composeTestRule.onNodeWithText(item2, substring = true)
+            .performClick()
+        composeTestRule.onNodeWithText(item2, substring = true)
+            .assertIsDisplayed()
+    }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBoxTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBoxTest.kt
new file mode 100644
index 0000000..58bc722
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBoxTest.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.settingslib.spa.widget.editor
+
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.isFocused
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SettingsExposedDropdownMenuCheckBoxTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+    private val item1 = "item1"
+    private val item2 = "item2"
+    private val item3 = "item3"
+    private val options = listOf(item1, item2, item3)
+    private val selectedOptionsState1 = mutableStateListOf(item1, item2)
+    private val exposedDropdownMenuCheckBoxLabel = "ExposedDropdownMenuCheckBoxLabel"
+
+    @Test
+    fun exposedDropdownMenuCheckBox_displayed() {
+        composeTestRule.setContent {
+            SettingsExposedDropdownMenuCheckBox(label = exposedDropdownMenuCheckBoxLabel,
+                options = options,
+                selectedOptionsState = remember { selectedOptionsState1 },
+                enabled = true,
+                onselectedOptionStateChange = {})
+        }
+        composeTestRule.onNodeWithText(
+            exposedDropdownMenuCheckBoxLabel, substring = true
+        ).assertIsDisplayed()
+    }
+
+    @Test
+    fun exposedDropdownMenuCheckBox_expanded() {
+        composeTestRule.setContent {
+            SettingsExposedDropdownMenuCheckBox(label = exposedDropdownMenuCheckBoxLabel,
+                options = options,
+                selectedOptionsState = remember { selectedOptionsState1 },
+                enabled = true,
+                onselectedOptionStateChange = {})
+        }
+        composeTestRule.onNodeWithText(item3, substring = true).assertDoesNotExist()
+        composeTestRule.onNodeWithText(exposedDropdownMenuCheckBoxLabel, substring = true)
+            .performClick()
+        composeTestRule.onNodeWithText(item3, substring = true).assertIsDisplayed()
+    }
+
+    @Test
+    fun exposedDropdownMenuCheckBox_valueAdded() {
+        composeTestRule.setContent {
+            SettingsExposedDropdownMenuCheckBox(label = exposedDropdownMenuCheckBoxLabel,
+                options = options,
+                selectedOptionsState = remember { selectedOptionsState1 },
+                enabled = true,
+                onselectedOptionStateChange = {})
+        }
+        composeTestRule.onNodeWithText(item3, substring = true).assertDoesNotExist()
+        composeTestRule.onNodeWithText(exposedDropdownMenuCheckBoxLabel, substring = true)
+            .performClick()
+        composeTestRule.onNodeWithText(item3, substring = true).performClick()
+        composeTestRule.onFocusedText(item3).assertIsDisplayed()
+    }
+
+    @Test
+    fun exposedDropdownMenuCheckBox_valueDeleted() {
+        composeTestRule.setContent {
+            SettingsExposedDropdownMenuCheckBox(label = exposedDropdownMenuCheckBoxLabel,
+                options = options,
+                selectedOptionsState = remember { selectedOptionsState1 },
+                enabled = true,
+                onselectedOptionStateChange = {})
+        }
+        composeTestRule.onNodeWithText(item2, substring = true).assertIsDisplayed()
+        composeTestRule.onNodeWithText(exposedDropdownMenuCheckBoxLabel, substring = true)
+            .performClick()
+        composeTestRule.onNotFocusedText(item2).performClick()
+        composeTestRule.onFocusedText(item2).assertDoesNotExist()
+    }
+}
+
+fun ComposeContentTestRule.onFocusedText(text: String): SemanticsNodeInteraction =
+    onNode(isFocused() and hasText(text, substring = true))
+
+fun ComposeContentTestRule.onNotFocusedText(text: String): SemanticsNodeInteraction =
+    onNode(!isFocused() and hasText(text, substring = true))
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsOutlinedTextFieldsTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsOutlinedTextFieldsTest.kt
new file mode 100644
index 0000000..5eff1a2
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsOutlinedTextFieldsTest.kt
@@ -0,0 +1,98 @@
+/*
+ * 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.settingslib.spa.widget.editor
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsEnabled
+import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.assertTextContains
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performTextReplacement
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SettingsOutlinedTextFieldsTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+    private val outlinedTextFieldLabel = "OutlinedTextField Enabled"
+    private val enabledValue = "Enabled Value"
+    private val disabledValue = "Disabled Value"
+    private val valueChanged = "Value Changed"
+
+    @Test
+    fun outlinedTextField_displayed() {
+        composeTestRule.setContent {
+            SettingsOutlinedTextField(
+                value = enabledValue,
+                label = outlinedTextFieldLabel,
+                enabled = true,
+                onTextChange = {})
+        }
+        composeTestRule.onNodeWithText(outlinedTextFieldLabel, substring = true)
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun outlinedTextFields_enabled() {
+        composeTestRule.setContent {
+            SettingsOutlinedTextField(
+                value = enabledValue,
+                label = outlinedTextFieldLabel,
+                enabled = true,
+                onTextChange = {})
+        }
+        composeTestRule.onNodeWithText(outlinedTextFieldLabel, substring = true)
+            .assertIsEnabled()
+    }
+
+    @Test
+    fun outlinedTextFields_disabled() {
+        composeTestRule.setContent {
+            SettingsOutlinedTextField(
+                value = disabledValue,
+                label = outlinedTextFieldLabel,
+                enabled = false,
+                onTextChange = {})
+        }
+        composeTestRule.onNodeWithText(outlinedTextFieldLabel, substring = true)
+            .assertIsNotEnabled()
+    }
+
+    @Test
+    fun outlinedTextFields_inputValue() {
+        composeTestRule.setContent {
+            var value by remember { mutableStateOf(enabledValue) }
+            SettingsOutlinedTextField(
+                value = value,
+                label = outlinedTextFieldLabel,
+                enabled = true,
+                onTextChange = { value = it })
+        }
+        composeTestRule.onNodeWithText(outlinedTextFieldLabel, substring = true)
+            .performTextReplacement(valueChanged)
+        composeTestRule.onNodeWithText(outlinedTextFieldLabel, substring = true)
+            .assertTextContains(valueChanged)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
index 8064cc1..8954947 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
@@ -107,7 +107,7 @@
     val isBypassEnabled: Flow<Boolean>
 
     /** Set whether face authentication should be locked out or not */
-    fun lockoutFaceAuth()
+    fun setLockedOut(isLockedOut: Boolean)
 
     /**
      * Cancel current face authentication and prevent it from running until [resumeFaceAuth] is
@@ -228,8 +228,8 @@
         }
             ?: flowOf(false)
 
-    override fun lockoutFaceAuth() {
-        _isLockedOut.value = true
+    override fun setLockedOut(isLockedOut: Boolean) {
+        _isLockedOut.value = isLockedOut
     }
 
     private val faceLockoutResetCallback =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt
index e4e6a6d..46135fa 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt
@@ -55,7 +55,7 @@
     override val isBypassEnabled: Flow<Boolean>
         get() = emptyFlow()
 
-    override fun lockoutFaceAuth() = Unit
+    override fun setLockedOut(isLockedOut: Boolean) = Unit
     override fun pauseFaceAuth() = Unit
 
     override fun resumeFaceAuth() = Unit
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt
index 4b8171f..ccc2080 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt
@@ -132,7 +132,7 @@
             .onEach {
                 if (it) {
                     faceAuthenticationLogger.faceLockedOut("Fingerprint locked out")
-                    repository.lockoutFaceAuth()
+                    repository.setLockedOut(true)
                 }
             }
             .launchIn(applicationScope)
@@ -148,9 +148,9 @@
                     repository.pauseFaceAuth()
                 } else if (wasSwitching && !isSwitching) {
                     val lockoutMode = facePropertyRepository.getLockoutMode(curr.userInfo.id)
-                    if (lockoutMode == LockoutMode.PERMANENT || lockoutMode == LockoutMode.TIMED) {
-                        repository.lockoutFaceAuth()
-                    }
+                    repository.setLockedOut(
+                        lockoutMode == LockoutMode.PERMANENT || lockoutMode == LockoutMode.TIMED
+                    )
                     repository.resumeFaceAuth()
                     yield()
                     runFaceAuth(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
index f0dbaf1..ec15416 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
@@ -559,7 +559,7 @@
 
     @Test
     fun authenticateDoesNotRunWhenFaceIsDisabled() =
-        testScope.runTest { testGatingCheckForFaceAuth { underTest.lockoutFaceAuth() } }
+        testScope.runTest { testGatingCheckForFaceAuth { underTest.setLockedOut(true) } }
 
     @Test
     fun authenticateDoesNotRunWhenUserIsCurrentlyTrusted() =
@@ -869,7 +869,7 @@
             initCollectors()
             assertThat(underTest.isLockedOut.value).isFalse()
 
-            underTest.lockoutFaceAuth()
+            underTest.setLockedOut(true)
             runCurrent()
 
             assertThat(underTest.isLockedOut.value).isTrue()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt
index ec11573..da70a9f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt
@@ -244,15 +244,18 @@
         testScope.runTest {
             underTest.start()
 
-            // previously running
+            // User switching has started
+            fakeUserRepository.setSelectedUserInfo(primaryUser, SelectionStatus.SELECTION_COMPLETE)
             fakeUserRepository.setSelectedUserInfo(
                 primaryUser,
                 SelectionStatus.SELECTION_IN_PROGRESS
             )
             runCurrent()
-            bouncerRepository.setPrimaryShow(true)
+            assertThat(faceAuthRepository.isFaceAuthPaused()).isTrue()
 
-            facePropertyRepository.setLockoutMode(secondaryUser.id, LockoutMode.TIMED)
+            bouncerRepository.setPrimaryShow(true)
+            // New user is not locked out.
+            facePropertyRepository.setLockoutMode(secondaryUser.id, LockoutMode.NONE)
             fakeUserRepository.setSelectedUserInfo(
                 secondaryUser,
                 SelectionStatus.SELECTION_COMPLETE
@@ -260,7 +263,7 @@
             runCurrent()
 
             assertThat(faceAuthRepository.isFaceAuthPaused()).isFalse()
-            assertThat(faceAuthRepository.wasDisabled).isTrue()
+            assertThat(faceAuthRepository.isLockedOut.value).isFalse()
 
             runCurrent()
             assertThat(faceAuthRepository.runningAuthRequest.value!!.first)
@@ -406,7 +409,7 @@
             fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
             runCurrent()
 
-            assertThat(faceAuthRepository.wasDisabled).isTrue()
+            assertThat(faceAuthRepository.isLockedOut.value).isTrue()
         }
 
     companion object {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt
index 1e1dc4f..2b13dca 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt
@@ -27,11 +27,6 @@
 
 class FakeDeviceEntryFaceAuthRepository : DeviceEntryFaceAuthRepository {
 
-    private var _wasDisabled: Boolean = false
-
-    val wasDisabled: Boolean
-        get() = _wasDisabled
-
     override val isAuthenticated = MutableStateFlow(false)
     override val canRunFaceAuth = MutableStateFlow(false)
     private val _authenticationStatus = MutableStateFlow<FaceAuthenticationStatus?>(null)
@@ -57,8 +52,8 @@
     override val isAuthRunning: StateFlow<Boolean> = _isAuthRunning
 
     override val isBypassEnabled = MutableStateFlow(false)
-    override fun lockoutFaceAuth() {
-        _wasDisabled = true
+    override fun setLockedOut(isLockedOut: Boolean) {
+        _isLockedOut.value = isLockedOut
     }
 
     private val faceAuthPaused = MutableStateFlow(false)
@@ -79,10 +74,6 @@
         _isAuthRunning.value = true
     }
 
-    fun setLockedOut(value: Boolean) {
-        _isLockedOut.value = value
-    }
-
     override fun cancel() {
         _isAuthRunning.value = false
         _runningAuthRequest.value = null
diff --git a/services/core/java/com/android/server/trust/TrustAgentWrapper.java b/services/core/java/com/android/server/trust/TrustAgentWrapper.java
index 0043122..3abebf8 100644
--- a/services/core/java/com/android/server/trust/TrustAgentWrapper.java
+++ b/services/core/java/com/android/server/trust/TrustAgentWrapper.java
@@ -49,6 +49,7 @@
 import android.util.Slog;
 
 import com.android.internal.infra.AndroidFuture;
+import com.android.server.utils.Slogf;
 
 import java.util.Collections;
 import java.util.List;
@@ -324,8 +325,8 @@
                 int flags,
                 AndroidFuture resultCallback) {
             if (DEBUG) {
-                Slog.d(TAG, "enableTrust(" + message + ", durationMs = " + durationMs
-                        + ", flags = " + flags + ")");
+                Slogf.d(TAG, "grantTrust(message=\"%s\", durationMs=%d, flags=0x%x)",
+                        message, durationMs, flags);
             }
 
             Message msg = mHandler.obtainMessage(
@@ -342,30 +343,32 @@
 
         @Override
         public void lockUser() {
+            if (DEBUG) Slog.d(TAG, "lockUser()");
             mHandler.sendEmptyMessage(MSG_LOCK_USER);
         }
 
         @Override
         public void setManagingTrust(boolean managingTrust) {
-            if (DEBUG) Slog.d(TAG, "managingTrust()");
+            if (DEBUG) Slogf.d(TAG, "setManagingTrust(%s)", managingTrust);
             mHandler.obtainMessage(MSG_MANAGING_TRUST, managingTrust ? 1 : 0, 0).sendToTarget();
         }
 
         @Override
         public void onConfigureCompleted(boolean result, IBinder token) {
-            if (DEBUG) Slog.d(TAG, "onSetTrustAgentFeaturesEnabledCompleted(result=" + result);
+            if (DEBUG) Slogf.d(TAG, "onConfigureCompleted(result=%s)", result);
             mHandler.obtainMessage(MSG_SET_TRUST_AGENT_FEATURES_COMPLETED,
                     result ? 1 : 0, 0, token).sendToTarget();
         }
 
         @Override
         public void addEscrowToken(byte[] token, int userId) {
+            // 'token' is secret; never log it.
+            if (DEBUG) Slogf.d(TAG, "addEscrowToken(userId=%d)", userId);
+
             if (mContext.getResources()
                     .getBoolean(com.android.internal.R.bool.config_allowEscrowTokenForTrustAgent)) {
-                throw  new SecurityException("Escrow token API is not allowed.");
+                throw new SecurityException("Escrow token API is not allowed.");
             }
-
-            if (DEBUG) Slog.d(TAG, "adding escrow token for user " + userId);
             Message msg = mHandler.obtainMessage(MSG_ADD_ESCROW_TOKEN);
             msg.getData().putByteArray(DATA_ESCROW_TOKEN, token);
             msg.getData().putInt(DATA_USER_ID, userId);
@@ -374,12 +377,12 @@
 
         @Override
         public void isEscrowTokenActive(long handle, int userId) {
+            if (DEBUG) Slogf.d(TAG, "isEscrowTokenActive(handle=%016x, userId=%d)", handle, userId);
+
             if (mContext.getResources()
                     .getBoolean(com.android.internal.R.bool.config_allowEscrowTokenForTrustAgent)) {
                 throw new SecurityException("Escrow token API is not allowed.");
             }
-
-            if (DEBUG) Slog.d(TAG, "checking the state of escrow token on user " + userId);
             Message msg = mHandler.obtainMessage(MSG_ESCROW_TOKEN_STATE);
             msg.getData().putLong(DATA_HANDLE, handle);
             msg.getData().putInt(DATA_USER_ID, userId);
@@ -388,12 +391,12 @@
 
         @Override
         public void removeEscrowToken(long handle, int userId) {
+            if (DEBUG) Slogf.d(TAG, "removeEscrowToken(handle=%016x, userId=%d)", handle, userId);
+
             if (mContext.getResources()
                     .getBoolean(com.android.internal.R.bool.config_allowEscrowTokenForTrustAgent)) {
                 throw new SecurityException("Escrow token API is not allowed.");
             }
-
-            if (DEBUG) Slog.d(TAG, "removing escrow token on user " + userId);
             Message msg = mHandler.obtainMessage(MSG_REMOVE_ESCROW_TOKEN);
             msg.getData().putLong(DATA_HANDLE, handle);
             msg.getData().putInt(DATA_USER_ID, userId);
@@ -402,12 +405,13 @@
 
         @Override
         public void unlockUserWithToken(long handle, byte[] token, int userId) {
+            // 'token' is secret; never log it.
+            if (DEBUG) Slogf.d(TAG, "unlockUserWithToken(handle=%016x, userId=%d)", handle, userId);
+
             if (mContext.getResources()
                     .getBoolean(com.android.internal.R.bool.config_allowEscrowTokenForTrustAgent)) {
                 throw new SecurityException("Escrow token API is not allowed.");
             }
-
-            if (DEBUG) Slog.d(TAG, "unlocking user " + userId);
             Message msg = mHandler.obtainMessage(MSG_UNLOCK_USER);
             msg.getData().putInt(DATA_USER_ID, userId);
             msg.getData().putLong(DATA_HANDLE, handle);
@@ -417,7 +421,7 @@
 
         @Override
         public void showKeyguardErrorMessage(CharSequence message) {
-            if (DEBUG) Slog.d(TAG, "Showing keyguard error message: " + message);
+            if (DEBUG) Slogf.d(TAG, "showKeyguardErrorMessage(\"%s\")", message);
             Message msg = mHandler.obtainMessage(MSG_SHOW_KEYGUARD_ERROR_MESSAGE);
             msg.getData().putCharSequence(DATA_MESSAGE, message);
             msg.sendToTarget();
diff --git a/services/tests/wmtests/src/com/android/server/policy/SingleKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/SingleKeyGestureTests.java
index b9492e9..d388db8 100644
--- a/services/tests/wmtests/src/com/android/server/policy/SingleKeyGestureTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/SingleKeyGestureTests.java
@@ -74,10 +74,11 @@
 
     @Before
     public void setUp() {
-        mInstrumentation.runOnMainSync(() -> {
-            mDetector = SingleKeyGestureDetector.get(mContext);
-            initSingleKeyGestureRules();
-        });
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mDetector = SingleKeyGestureDetector.get(mContext);
+                    initSingleKeyGestureRules();
+                });
 
         mWaitTimeout = SingleKeyGestureDetector.MULTI_PRESS_TIMEOUT + 50;
         mLongPressTime = SingleKeyGestureDetector.sDefaultLongPressTimeout + 50;
@@ -85,84 +86,91 @@
     }
 
     private void initSingleKeyGestureRules() {
-        mDetector.addRule(new SingleKeyGestureDetector.SingleKeyRule(KEYCODE_POWER) {
-            @Override
-            boolean supportLongPress() {
-                return mLongPressOnPowerBehavior;
-            }
-            @Override
-            boolean supportVeryLongPress() {
-                return mVeryLongPressOnPowerBehavior;
-            }
-            @Override
-            int getMaxMultiPressCount() {
-                return mMaxMultiPressCount;
-            }
-            @Override
-            public void onPress(long downTime) {
-                if (mDetector.beganFromNonInteractive() && !mAllowNonInteractiveForPress) {
-                    return;
-                }
-                mShortPressed.countDown();
-            }
+        mDetector.addRule(
+                new SingleKeyGestureDetector.SingleKeyRule(KEYCODE_POWER) {
+                    @Override
+                    boolean supportLongPress() {
+                        return mLongPressOnPowerBehavior;
+                    }
 
-            @Override
-            void onLongPress(long downTime) {
-                if (mDetector.beganFromNonInteractive() && !mAllowNonInteractiveForLongPress) {
-                    return;
-                }
-                mLongPressed.countDown();
-            }
+                    @Override
+                    boolean supportVeryLongPress() {
+                        return mVeryLongPressOnPowerBehavior;
+                    }
 
-            @Override
-            void onVeryLongPress(long downTime) {
-                mVeryLongPressed.countDown();
-            }
+                    @Override
+                    int getMaxMultiPressCount() {
+                        return mMaxMultiPressCount;
+                    }
 
-            @Override
-            void onMultiPress(long downTime, int count) {
-                if (mDetector.beganFromNonInteractive() && !mAllowNonInteractiveForPress) {
-                    return;
-                }
-                mMultiPressed.countDown();
-                assertTrue(mMaxMultiPressCount >= count);
-                assertEquals(mExpectedMultiPressCount, count);
-            }
-        });
+                    @Override
+                    public void onPress(long downTime) {
+                        if (mDetector.beganFromNonInteractive() && !mAllowNonInteractiveForPress) {
+                            return;
+                        }
+                        mShortPressed.countDown();
+                    }
 
-        mDetector.addRule(new SingleKeyGestureDetector.SingleKeyRule(KEYCODE_BACK) {
-            @Override
-            boolean supportLongPress() {
-                return mLongPressOnBackBehavior;
-            }
-            @Override
-            int getMaxMultiPressCount() {
-                return mMaxMultiPressCount;
-            }
-            @Override
-            public void onPress(long downTime) {
-                if (mDetector.beganFromNonInteractive() && !mAllowNonInteractiveForPress) {
-                    return;
-                }
-                mShortPressed.countDown();
-            }
+                    @Override
+                    void onLongPress(long downTime) {
+                        if (mDetector.beganFromNonInteractive()
+                                && !mAllowNonInteractiveForLongPress) {
+                            return;
+                        }
+                        mLongPressed.countDown();
+                    }
 
-            @Override
-            void onMultiPress(long downTime, int count) {
-                if (mDetector.beganFromNonInteractive() && !mAllowNonInteractiveForPress) {
-                    return;
-                }
-                mMultiPressed.countDown();
-                assertTrue(mMaxMultiPressCount >= count);
-                assertEquals(mExpectedMultiPressCount, count);
-            }
+                    @Override
+                    void onVeryLongPress(long downTime) {
+                        mVeryLongPressed.countDown();
+                    }
 
-            @Override
-            void onLongPress(long downTime) {
-                mLongPressed.countDown();
-            }
-        });
+                    @Override
+                    void onMultiPress(long downTime, int count) {
+                        if (mDetector.beganFromNonInteractive() && !mAllowNonInteractiveForPress) {
+                            return;
+                        }
+                        mMultiPressed.countDown();
+                        assertTrue(mMaxMultiPressCount >= count);
+                        assertEquals(mExpectedMultiPressCount, count);
+                    }
+                });
 
+        mDetector.addRule(
+                new SingleKeyGestureDetector.SingleKeyRule(KEYCODE_BACK) {
+                    @Override
+                    boolean supportLongPress() {
+                        return mLongPressOnBackBehavior;
+                    }
+
+                    @Override
+                    int getMaxMultiPressCount() {
+                        return mMaxMultiPressCount;
+                    }
+
+                    @Override
+                    public void onPress(long downTime) {
+                        if (mDetector.beganFromNonInteractive() && !mAllowNonInteractiveForPress) {
+                            return;
+                        }
+                        mShortPressed.countDown();
+                    }
+
+                    @Override
+                    void onMultiPress(long downTime, int count) {
+                        if (mDetector.beganFromNonInteractive() && !mAllowNonInteractiveForPress) {
+                            return;
+                        }
+                        mMultiPressed.countDown();
+                        assertTrue(mMaxMultiPressCount >= count);
+                        assertEquals(mExpectedMultiPressCount, count);
+                    }
+
+                    @Override
+                    void onLongPress(long downTime) {
+                        mLongPressed.countDown();
+                    }
+                });
     }
 
     private void pressKey(int keyCode, long pressTime) {
@@ -176,8 +184,14 @@
     private void pressKey(
             int keyCode, long pressTime, boolean interactive, boolean defaultDisplayOn) {
         long eventTime = SystemClock.uptimeMillis();
-        final KeyEvent keyDown = new KeyEvent(eventTime, eventTime, ACTION_DOWN,
-                keyCode, 0 /* repeat */, 0 /* metaState */);
+        final KeyEvent keyDown =
+                new KeyEvent(
+                        eventTime,
+                        eventTime,
+                        ACTION_DOWN,
+                        keyCode,
+                        0 /* repeat */,
+                        0 /* metaState */);
         mDetector.interceptKey(keyDown, interactive, defaultDisplayOn);
 
         // keep press down.
@@ -188,8 +202,14 @@
         }
 
         eventTime += pressTime;
-        final KeyEvent keyUp = new KeyEvent(eventTime, eventTime, ACTION_UP,
-                keyCode, 0 /* repeat */, 0 /* metaState */);
+        final KeyEvent keyUp =
+                new KeyEvent(
+                        eventTime,
+                        eventTime,
+                        ACTION_UP,
+                        keyCode,
+                        0 /* repeat */,
+                        0 /* metaState */);
 
         mDetector.interceptKey(keyUp, interactive, defaultDisplayOn);
     }
@@ -252,10 +272,12 @@
             // To make sure we won't get any crash while panic pressing keys.
             for (int i = 0; i < 100; i++) {
                 mShortPressed = new CountDownLatch(2);
-                newHandler.runWithScissors(() -> {
-                    pressKey(KEYCODE_POWER, 0 /* pressTime */);
-                    pressKey(KEYCODE_BACK, 0 /* pressTime */);
-                }, mWaitTimeout);
+                newHandler.runWithScissors(
+                        () -> {
+                            pressKey(KEYCODE_POWER, 0 /* pressTime */);
+                            pressKey(KEYCODE_BACK, 0 /* pressTime */);
+                        },
+                        mWaitTimeout);
                 assertTrue(mShortPressed.await(mWaitTimeout, TimeUnit.MILLISECONDS));
             }
         } finally {
@@ -274,15 +296,16 @@
             for (int i = 0; i < 5; i++) {
                 mMultiPressed = new CountDownLatch(1);
                 mShortPressed = new CountDownLatch(1);
-                newHandler.runWithScissors(() -> {
-                    pressKey(KEYCODE_POWER, 0 /* pressTime */);
-                    pressKey(KEYCODE_POWER, 0 /* pressTime */);
-                }, mWaitTimeout);
+                newHandler.runWithScissors(
+                        () -> {
+                            pressKey(KEYCODE_POWER, 0 /* pressTime */);
+                            pressKey(KEYCODE_POWER, 0 /* pressTime */);
+                        },
+                        mWaitTimeout);
                 assertTrue(mMultiPressed.await(mWaitTimeout, TimeUnit.MILLISECONDS));
 
-                newHandler.runWithScissors(() -> {
-                    pressKey(KEYCODE_POWER, 0 /* pressTime */);
-                }, mWaitTimeout);
+                newHandler.runWithScissors(
+                        () -> pressKey(KEYCODE_POWER, 0 /* pressTime */), mWaitTimeout);
                 assertTrue(mShortPressed.await(mWaitTimeout, TimeUnit.MILLISECONDS));
             }
         } finally {
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 0c8603b3..66f3bed 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -3530,7 +3530,7 @@
     /**
      * When a partial sms / mms message stay in raw table for too long without being completed,
      * we expire them and delete them from the raw table. This carrier config defines the
-     * expiration time.
+     * expiration time. The default value is milliseconds in 7 days.
      * @hide
      */
     public static final String KEY_UNDELIVERED_SMS_MESSAGE_EXPIRATION_TIME =
@@ -10204,6 +10204,10 @@
         sDefaults.putBoolean(KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL, false);
         sDefaults.putBoolean(KEY_RTT_SUPPORTED_BOOL, false);
         sDefaults.putBoolean(KEY_TTY_SUPPORTED_BOOL, true);
+        sDefaults.putBoolean(KEY_RTT_AUTO_UPGRADE_BOOL, false);
+        sDefaults.putBoolean(KEY_RTT_SUPPORTED_FOR_VT_BOOL, false);
+        sDefaults.putBoolean(KEY_RTT_UPGRADE_SUPPORTED_BOOL, false);
+        sDefaults.putBoolean(KEY_RTT_DOWNGRADE_SUPPORTED_BOOL, false);
         sDefaults.putBoolean(KEY_HIDE_TTY_HCO_VCO_WITH_RTT_BOOL, false);
         sDefaults.putBoolean(KEY_RTT_SUPPORTED_WHILE_ROAMING_BOOL, false);
         sDefaults.putBoolean(KEY_RTT_UPGRADE_SUPPORTED_FOR_DOWNGRADED_VT_CALL_BOOL, true);
@@ -10324,6 +10328,8 @@
                 "NR_NSA_MMWAVE:145000,60000", "NR_SA:145000,60000", "NR_SA_MMWAVE:145000,60000"});
         sDefaults.putBoolean(KEY_BANDWIDTH_NR_NSA_USE_LTE_VALUE_FOR_UPLINK_BOOL, false);
         sDefaults.putString(KEY_WCDMA_DEFAULT_SIGNAL_STRENGTH_MEASUREMENT_STRING, "rssi");
+        sDefaults.putLong(
+                KEY_UNDELIVERED_SMS_MESSAGE_EXPIRATION_TIME, (long) (60 * 60 * 1000) * 24 * 7);
         sDefaults.putBoolean(KEY_CONFIG_SHOW_ORIG_DIAL_STRING_FOR_CDMA_BOOL, false);
         sDefaults.putBoolean(KEY_SHOW_CALL_BLOCKING_DISABLED_NOTIFICATION_ALWAYS_BOOL, false);
         sDefaults.putBoolean(KEY_CALL_FORWARDING_OVER_UT_WARNING_BOOL, false);
diff --git a/tests/FlickerTests/AndroidTestTemplate.xml b/tests/FlickerTests/AndroidTestTemplate.xml
index 8780385..63acddf 100644
--- a/tests/FlickerTests/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/AndroidTestTemplate.xml
@@ -90,7 +90,7 @@
         <option name="directory-keys"
                 value="/data/user/0/com.android.server.wm.flicker.notification/files"/>
         <option name="directory-keys"
-            value="/data/user/0/com.android.server.wm.flicker.service/files"/>
+                value="/data/user/0/com.android.server.wm.flicker.service/files"/>
         <option name="collect-on-run-ended-only" value="true"/>
         <option name="clean-up" value="true"/>
     </metrics_collector>