diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 05345d88f771..63f0b9ebca36 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -6100,6 +6100,15 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean @TestApi public static final String POINTER_SPEED = "pointer_speed"; + /** + * Pointer scale setting. + * + *

This float value represents the scale by which the size of the pointer increases. + * @hide + */ + @Readable + public static final String POINTER_SCALE = "pointer_scale"; + /** * Touchpad pointer speed setting. * This is an integer value in a range between -7 and +7, so there are 15 possible values. @@ -6358,6 +6367,7 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean PRIVATE_SETTINGS.add(SIP_ASK_ME_EACH_TIME); PRIVATE_SETTINGS.add(POINTER_SPEED); PRIVATE_SETTINGS.add(POINTER_FILL_STYLE); + PRIVATE_SETTINGS.add(POINTER_SCALE); PRIVATE_SETTINGS.add(LOCK_TO_APP_ENABLED); PRIVATE_SETTINGS.add(EGG_MODE); PRIVATE_SETTINGS.add(SHOW_BATTERY_PERCENT); diff --git a/core/java/android/view/PointerIcon.java b/core/java/android/view/PointerIcon.java index 7c2577fdf8e1..c30212657c57 100644 --- a/core/java/android/view/PointerIcon.java +++ b/core/java/android/view/PointerIcon.java @@ -193,6 +193,9 @@ public final class PointerIcon implements Parcelable { /** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_END = POINTER_ICON_VECTOR_STYLE_FILL_BLUE; + /** @hide */ public static final float DEFAULT_POINTER_SCALE = 1f; + /** @hide */ public static final float LARGE_POINTER_SCALE = 2.5f; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) private final int mType; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @@ -253,7 +256,7 @@ private PointerIcon(int type) { * @hide */ public static @NonNull PointerIcon getLoadedSystemIcon(@NonNull Context context, int type, - boolean useLargeIcons) { + boolean useLargeIcons, float pointerScale) { if (type == TYPE_NOT_SPECIFIED) { throw new IllegalStateException("Cannot load icon for type TYPE_NOT_SPECIFIED"); } @@ -268,13 +271,18 @@ private PointerIcon(int type) { } final int defStyle; - // TODO(b/305193969): Use scaled vectors when large icons are requested. - if (useLargeIcons) { - defStyle = com.android.internal.R.style.LargePointer; - } else if (android.view.flags.Flags.enableVectorCursors()) { + if (android.view.flags.Flags.enableVectorCursorA11ySettings()) { defStyle = com.android.internal.R.style.VectorPointer; } else { - defStyle = com.android.internal.R.style.Pointer; + // TODO(b/346358375): Remove useLargeIcons and the legacy pointer styles when + // enableVectorCursorA11ySetting is rolled out. + if (useLargeIcons) { + defStyle = com.android.internal.R.style.LargePointer; + } else if (android.view.flags.Flags.enableVectorCursors()) { + defStyle = com.android.internal.R.style.VectorPointer; + } else { + defStyle = com.android.internal.R.style.Pointer; + } } TypedArray a = context.obtainStyledAttributes(null, com.android.internal.R.styleable.Pointer, @@ -286,11 +294,11 @@ private PointerIcon(int type) { Log.w(TAG, "Missing theme resources for pointer icon type " + type); return type == TYPE_DEFAULT ? getSystemIcon(TYPE_NULL) - : getLoadedSystemIcon(context, TYPE_DEFAULT, useLargeIcons); + : getLoadedSystemIcon(context, TYPE_DEFAULT, useLargeIcons, pointerScale); } final PointerIcon icon = new PointerIcon(type); - icon.loadResource(context.getResources(), resourceId, context.getTheme()); + icon.loadResource(context.getResources(), resourceId, context.getTheme(), pointerScale); return icon; } @@ -353,7 +361,7 @@ private boolean isLoaded() { } PointerIcon icon = new PointerIcon(TYPE_CUSTOM); - icon.loadResource(resources, resourceId, null); + icon.loadResource(resources, resourceId, null, DEFAULT_POINTER_SCALE); return icon; } @@ -460,12 +468,13 @@ private Bitmap getBitmapFromDrawable(BitmapDrawable bitmapDrawable) { } private BitmapDrawable getBitmapDrawableFromVectorDrawable(Resources resources, - VectorDrawable vectorDrawable) { + VectorDrawable vectorDrawable, float pointerScale) { // Ensure we pass the display metrics into the Bitmap constructor so that it is initialized // with the correct density. Bitmap bitmap = Bitmap.createBitmap(resources.getDisplayMetrics(), - vectorDrawable.getIntrinsicWidth(), - vectorDrawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888, true /* hasAlpha */); + (int) (vectorDrawable.getIntrinsicWidth() * pointerScale), + (int) (vectorDrawable.getIntrinsicHeight() * pointerScale), + Bitmap.Config.ARGB_8888, true /* hasAlpha */); Canvas canvas = new Canvas(bitmap); vectorDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); vectorDrawable.draw(canvas); @@ -473,7 +482,7 @@ private BitmapDrawable getBitmapDrawableFromVectorDrawable(Resources resources, } private void loadResource(@NonNull Resources resources, @XmlRes int resourceId, - @Nullable Resources.Theme theme) { + @Nullable Resources.Theme theme, float pointerScale) { final XmlResourceParser parser = resources.getXml(resourceId); final int bitmapRes; final float hotSpotX; @@ -484,8 +493,10 @@ private void loadResource(@NonNull Resources resources, @XmlRes int resourceId, final TypedArray a = resources.obtainAttributes( parser, com.android.internal.R.styleable.PointerIcon); bitmapRes = a.getResourceId(com.android.internal.R.styleable.PointerIcon_bitmap, 0); - hotSpotX = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotX, 0); - hotSpotY = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotY, 0); + hotSpotX = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotX, 0) + * pointerScale; + hotSpotY = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotY, 0) + * pointerScale; a.recycle(); } catch (Exception ex) { throw new IllegalArgumentException("Exception parsing pointer icon resource.", ex); @@ -534,7 +545,7 @@ private void loadResource(@NonNull Resources resources, @XmlRes int resourceId, } if (isVectorAnimation) { drawableFrame = getBitmapDrawableFromVectorDrawable(resources, - (VectorDrawable) drawableFrame); + (VectorDrawable) drawableFrame, pointerScale); } mBitmapFrames[i - 1] = getBitmapFromDrawable((BitmapDrawable) drawableFrame); } @@ -542,7 +553,8 @@ private void loadResource(@NonNull Resources resources, @XmlRes int resourceId, } if (drawable instanceof VectorDrawable) { mDrawNativeDropShadow = true; - drawable = getBitmapDrawableFromVectorDrawable(resources, (VectorDrawable) drawable); + drawable = getBitmapDrawableFromVectorDrawable(resources, (VectorDrawable) drawable, + pointerScale); } if (!(drawable instanceof BitmapDrawable)) { throw new IllegalArgumentException(" bitmap attribute must " diff --git a/core/proto/android/providers/settings/system.proto b/core/proto/android/providers/settings/system.proto index 123306924c2b..6a0ec1dcb3f2 100644 --- a/core/proto/android/providers/settings/system.proto +++ b/core/proto/android/providers/settings/system.proto @@ -126,6 +126,7 @@ message SystemSettingsProto { option (android.msg_privacy).dest = DEST_EXPLICIT; optional SettingProto pointer_fill_style = 1 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto pointer_scale = 3 [ (android.privacy).dest = DEST_AUTOMATIC ]; } optional Pointer pointer = 37; optional SettingProto pointer_speed = 18 [ (android.privacy).dest = DEST_AUTOMATIC ]; diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java index 5245456d98c2..00fb7a1feab1 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java @@ -80,6 +80,7 @@ private static String[] getSettingsToBackUp() { Settings.System.SIP_RECEIVE_CALLS, Settings.System.POINTER_SPEED, Settings.System.POINTER_FILL_STYLE, + Settings.System.POINTER_SCALE, Settings.System.VIBRATE_ON, Settings.System.VIBRATE_WHEN_RINGING, Settings.System.RINGTONE, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java index 2c3be4c10f5a..4235bc4157bf 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java @@ -26,6 +26,8 @@ import static android.provider.settings.validators.SettingsValidators.NON_NEGATIVE_INTEGER_VALIDATOR; import static android.provider.settings.validators.SettingsValidators.URI_VALIDATOR; import static android.provider.settings.validators.SettingsValidators.VIBRATION_INTENSITY_VALIDATOR; +import static android.view.PointerIcon.DEFAULT_POINTER_SCALE; +import static android.view.PointerIcon.LARGE_POINTER_SCALE; import static android.view.PointerIcon.POINTER_ICON_VECTOR_STYLE_FILL_BEGIN; import static android.view.PointerIcon.POINTER_ICON_VECTOR_STYLE_FILL_END; @@ -211,6 +213,8 @@ public boolean validate(String value) { VALIDATORS.put(System.POINTER_FILL_STYLE, new InclusiveIntegerRangeValidator(POINTER_ICON_VECTOR_STYLE_FILL_BEGIN, POINTER_ICON_VECTOR_STYLE_FILL_END)); + VALIDATORS.put(System.POINTER_SCALE, + new InclusiveFloatRangeValidator(DEFAULT_POINTER_SCALE, LARGE_POINTER_SCALE)); VALIDATORS.put(System.TOUCHPAD_POINTER_SPEED, new InclusiveIntegerRangeValidator(-7, 7)); VALIDATORS.put(System.TOUCHPAD_NATURAL_SCROLLING, BOOLEAN_VALIDATOR); VALIDATORS.put(System.TOUCHPAD_TAP_TO_CLICK, BOOLEAN_VALIDATOR); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index 625b8e4e2911..384cb7ee9c49 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -2915,6 +2915,9 @@ private static void dumpProtoSystemSettingsLocked( dumpSetting(s, p, Settings.System.POINTER_FILL_STYLE, SystemSettingsProto.Pointer.POINTER_FILL_STYLE); + dumpSetting(s, p, + Settings.System.POINTER_SCALE, + SystemSettingsProto.Pointer.POINTER_SCALE); p.end(pointerToken); dumpSetting(s, p, Settings.System.POINTER_SPEED, diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 0bd40d16ba4c..e5dbce9931a1 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -3352,6 +3352,10 @@ void setPointerFillStyle(@PointerIcon.PointerIconVectorStyleFill int fillStyle) mPointerIconCache.setPointerFillStyle(fillStyle); } + void setPointerScale(float scale) { + mPointerIconCache.setPointerScale(scale); + } + interface KeyboardBacklightControllerInterface { default void incrementKeyboardBacklight(int deviceId) {} default void decrementKeyboardBacklight(int deviceId) {} diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java index 9585b49be3a9..593b0917efc7 100644 --- a/services/core/java/com/android/server/input/InputSettingsObserver.java +++ b/services/core/java/com/android/server/input/InputSettingsObserver.java @@ -16,6 +16,7 @@ package com.android.server.input; +import static android.view.PointerIcon.DEFAULT_POINTER_SCALE; import static android.view.PointerIcon.POINTER_ICON_VECTOR_STYLE_FILL_BLACK; import static android.view.flags.Flags.enableVectorCursorA11ySettings; @@ -101,7 +102,9 @@ class InputSettingsObserver extends ContentObserver { Map.entry(Settings.Secure.getUriFor(Settings.Secure.STYLUS_POINTER_ICON_ENABLED), (reason) -> updateStylusPointerIconEnabled()), Map.entry(Settings.System.getUriFor(Settings.System.POINTER_FILL_STYLE), - (reason) -> updatePointerFillStyleFromSettings())); + (reason) -> updatePointerFillStyleFromSettings()), + Map.entry(Settings.System.getUriFor(Settings.System.POINTER_SCALE), + (reason) -> updatePointerScaleFromSettings())); } /** @@ -277,4 +280,14 @@ private void updatePointerFillStyleFromSettings() { UserHandle.USER_CURRENT); mService.setPointerFillStyle(pointerFillStyle); } + + private void updatePointerScaleFromSettings() { + if (!enableVectorCursorA11ySettings()) { + return; + } + final float pointerScale = Settings.System.getFloatForUser(mContext.getContentResolver(), + Settings.System.POINTER_SCALE, DEFAULT_POINTER_SCALE, + UserHandle.USER_CURRENT); + mService.setPointerScale(pointerScale); + } } diff --git a/services/core/java/com/android/server/input/PointerIconCache.java b/services/core/java/com/android/server/input/PointerIconCache.java index 936e17f51919..44622d80da0e 100644 --- a/services/core/java/com/android/server/input/PointerIconCache.java +++ b/services/core/java/com/android/server/input/PointerIconCache.java @@ -16,6 +16,7 @@ package com.android.server.input; +import static android.view.PointerIcon.DEFAULT_POINTER_SCALE; import static android.view.PointerIcon.POINTER_ICON_VECTOR_STYLE_FILL_BLACK; import android.annotation.NonNull; @@ -63,6 +64,8 @@ final class PointerIconCache { @GuardedBy("mLoadedPointerIconsByDisplayAndType") private @PointerIcon.PointerIconVectorStyleFill int mPointerIconFillStyle = POINTER_ICON_VECTOR_STYLE_FILL_BLACK; + @GuardedBy("mLoadedPointerIconsByDisplayAndType") + private float mPointerIconScale = DEFAULT_POINTER_SCALE; private final DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() { @@ -117,6 +120,11 @@ public void setPointerFillStyle(@PointerIcon.PointerIconVectorStyleFill int fill mUiThreadHandler.post(() -> handleSetPointerFillStyle(fillStyle)); } + /** Set the scale for vector pointer icons. */ + public void setPointerScale(float scale) { + mUiThreadHandler.post(() -> handleSetPointerScale(scale)); + } + /** * Get a loaded system pointer icon. This will fetch the icon from the cache, or load it if * it isn't already cached. @@ -137,7 +145,7 @@ public void setPointerFillStyle(@PointerIcon.PointerIconVectorStyleFill int fill theme.applyStyle(PointerIcon.vectorFillStyleToResource(mPointerIconFillStyle), /* force= */ true); icon = PointerIcon.getLoadedSystemIcon(new ContextThemeWrapper(context, theme), - type, mUseLargePointerIcons); + type, mUseLargePointerIcons, mPointerIconScale); iconsByType.put(type, icon); } return Objects.requireNonNull(icon); @@ -215,6 +223,19 @@ private void handleSetPointerFillStyle(@PointerIcon.PointerIconVectorStyleFill i mNative.reloadPointerIcons(); } + @android.annotation.UiThread + private void handleSetPointerScale(float scale) { + synchronized (mLoadedPointerIconsByDisplayAndType) { + if (mPointerIconScale == scale) { + return; + } + mPointerIconScale = scale; + // Clear all cached icons on all displays. + mLoadedPointerIconsByDisplayAndType.clear(); + } + mNative.reloadPointerIcons(); + } + // Updates the cached display density for the given displayId, and returns true if // the cached density changed. @GuardedBy("mLoadedPointerIconsByDisplayAndType") diff --git a/tests/Input/assets/testPointerScale.png b/tests/Input/assets/testPointerScale.png new file mode 100644 index 000000000000..54d37c24afc6 Binary files /dev/null and b/tests/Input/assets/testPointerScale.png differ diff --git a/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt b/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt index dac425329f48..d196b85a7466 100644 --- a/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt +++ b/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt @@ -93,7 +93,29 @@ class PointerIconLoadingTest { PointerIcon.getLoadedSystemIcon( ContextThemeWrapper(context, theme), PointerIcon.TYPE_ARROW, - /* useLargeIcons= */ false) + /* useLargeIcons= */ false, + /* pointerScale= */ 1f) + + pointerIcon.getBitmap().assertAgainstGolden( + screenshotRule, + testName.methodName, + exactScreenshotMatcher + ) + } + + @Test + fun testPointerScale() { + assumeTrue(enableVectorCursors()) + assumeTrue(enableVectorCursorA11ySettings()) + + val pointerScale = 2f + + val pointerIcon = + PointerIcon.getLoadedSystemIcon( + context, + PointerIcon.TYPE_ARROW, + /* useLargeIcons= */ false, + pointerScale) pointerIcon.getBitmap().assertAgainstGolden( screenshotRule,