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>