diff options
31 files changed, 1842 insertions, 1 deletions
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index c3fae55fd00c..88d7231bc7be 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -41,6 +41,7 @@ import android.view.InputDevice; import android.view.InputEvent; import android.view.InputMonitor; import android.view.PointerIcon; +import android.view.KeyCharacterMap; import android.view.VerifiedInputEvent; /** @hide */ @@ -63,6 +64,8 @@ interface IInputManager { // active keyboard layout. int getKeyCodeForKeyLocation(int deviceId, in int locationKeyCode); + KeyCharacterMap getKeyCharacterMap(String layoutDescriptor); + // Temporarily changes the pointer speed. void tryPointerSpeed(int speed); diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index a0cceae98ba9..ff1a6acd8e4e 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -16,6 +16,8 @@ package android.hardware.input; +import static com.android.hardware.input.Flags.keyboardLayoutPreviewFlag; + import android.Manifest; import android.annotation.FloatRange; import android.annotation.IntDef; @@ -31,6 +33,7 @@ import android.app.ActivityThread; import android.compat.annotation.ChangeId; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; +import android.graphics.drawable.Drawable; import android.hardware.BatteryState; import android.os.Build; import android.os.Handler; @@ -931,6 +934,31 @@ public final class InputManager { } /** + * Provides a Keyboard layout preview of a particular dimension. + * + * @param keyboardLayout Layout whose preview is requested. If null, will return preview of + * the default Keyboard layout defined by {@code Generic.kl}. + * @param width Expected width of the drawable + * @param height Expected height of the drawable + * + * NOTE: Width and height will auto-adjust to the width and height of the ImageView that + * shows the drawable but this allows the caller to provide an intrinsic width and height of + * the drawable allowing the ImageView to properly wrap the drawable content. + * + * @hide + */ + @Nullable + public Drawable getKeyboardLayoutPreview(@Nullable KeyboardLayout keyboardLayout, int width, + int height) { + if (!keyboardLayoutPreviewFlag()) { + return null; + } + PhysicalKeyLayout keyLayout = new PhysicalKeyLayout( + mGlobal.getKeyCharacterMap(keyboardLayout), keyboardLayout); + return new KeyboardLayoutPreviewDrawable(mContext, keyLayout, width, height); + } + + /** * Injects an input event into the event system, targeting windows owned by the provided uid. * * If a valid targetUid is provided, the system will only consider injecting the input event diff --git a/core/java/android/hardware/input/InputManagerGlobal.java b/core/java/android/hardware/input/InputManagerGlobal.java index e886f685263c..8c598aeae67c 100644 --- a/core/java/android/hardware/input/InputManagerGlobal.java +++ b/core/java/android/hardware/input/InputManagerGlobal.java @@ -51,6 +51,7 @@ import android.view.Display; import android.view.InputDevice; import android.view.InputEvent; import android.view.InputMonitor; +import android.view.KeyCharacterMap; import android.view.PointerIcon; import com.android.internal.annotations.GuardedBy; @@ -1206,6 +1207,21 @@ public final class InputManagerGlobal { } /** + * Returns KeyCharacterMap for the provided Keyboard layout. If provided layout is null it will + * return KeyCharacter map for the default layout {@code Generic.kl}. + */ + public KeyCharacterMap getKeyCharacterMap(@Nullable KeyboardLayout keyboardLayout) { + if (keyboardLayout == null) { + return KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); + } + try { + return mIm.getKeyCharacterMap(keyboardLayout.getDescriptor()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * @see InputManager#injectInputEvent(InputEvent, int, int) */ diff --git a/core/java/android/hardware/input/KeyboardLayout.java b/core/java/android/hardware/input/KeyboardLayout.java index 4403251e0488..bbfed24f9dc1 100644 --- a/core/java/android/hardware/input/KeyboardLayout.java +++ b/core/java/android/hardware/input/KeyboardLayout.java @@ -22,6 +22,7 @@ import android.os.Parcel; import android.os.Parcelable; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -230,6 +231,33 @@ public final class KeyboardLayout implements Parcelable, Comparable<KeyboardLayo return mProductId; } + /** + * Returns if the Keyboard layout follows the ANSI Physical key layout. + */ + public boolean isAnsiLayout() { + for (int i = 0; i < mLocales.size(); i++) { + Locale locale = mLocales.get(i); + if (locale != null && locale.getCountry().equalsIgnoreCase("us") + && mLayoutType != LayoutType.EXTENDED) { + return true; + } + } + return false; + } + + /** + * Returns if the Keyboard layout follows the JIS Physical key layout. + */ + public boolean isJisLayout() { + for (int i = 0; i < mLocales.size(); i++) { + Locale locale = mLocales.get(i); + if (locale != null && locale.getCountry().equalsIgnoreCase("jp")) { + return true; + } + } + return false; + } + @Override public int describeContents() { return 0; diff --git a/core/java/android/hardware/input/KeyboardLayoutPreviewDrawable.java b/core/java/android/hardware/input/KeyboardLayoutPreviewDrawable.java new file mode 100644 index 000000000000..d943c37e9e5b --- /dev/null +++ b/core/java/android/hardware/input/KeyboardLayoutPreviewDrawable.java @@ -0,0 +1,504 @@ +/* + * Copyright 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 android.annotation.ColorInt; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.util.Slog; +import android.util.TypedValue; +import android.view.KeyEvent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * A custom drawable class that draws preview of a Physical keyboard layout. + */ +final class KeyboardLayoutPreviewDrawable extends Drawable { + + private static final String TAG = "KeyboardLayoutPreview"; + private static final int GRAVITY_LEFT = 0x1; + private static final int GRAVITY_RIGHT = 0x2; + private static final int GRAVITY_TOP = 0x4; + private static final int GRAVITY_BOTTOM = 0x8; + private static final int GRAVITY_CENTER = + GRAVITY_LEFT | GRAVITY_RIGHT | GRAVITY_TOP | GRAVITY_BOTTOM; + private static final int GRAVITY_CENTER_HORIZONTAL = GRAVITY_LEFT | GRAVITY_RIGHT; + private static final int KEY_PADDING_IN_DP = 3; + private static final int KEYBOARD_PADDING_IN_DP = 10; + private static final int KEY_RADIUS_IN_DP = 5; + private static final int KEYBOARD_RADIUS_IN_DP = 10; + private static final int GLYPH_TEXT_SIZE_IN_SP = 10; + + private final List<KeyDrawable> mKeyDrawables = new ArrayList<>(); + + private final int mWidth; + private final int mHeight; + private final RectF mKeyboardBackground = new RectF(); + private final ResourceProvider mResourceProvider; + private final PhysicalKeyLayout mKeyLayout; + + public KeyboardLayoutPreviewDrawable(Context context, PhysicalKeyLayout keyLayout, int width, + int height) { + mWidth = width; + mHeight = height; + mResourceProvider = new ResourceProvider(context); + mKeyLayout = keyLayout; + } + + @Override + public int getIntrinsicWidth() { + return mWidth; + } + + @Override + public int getIntrinsicHeight() { + return mHeight; + } + + @Override + protected void onBoundsChange(@NonNull Rect bounds) { + super.onBoundsChange(bounds); + mKeyDrawables.clear(); + final PhysicalKeyLayout.LayoutKey[][] keys = mKeyLayout.getKeys(); + if (keys == null) { + return; + } + final PhysicalKeyLayout.EnterKey enterKey = mKeyLayout.getEnterKey(); + int width = bounds.width(); + int height = bounds.height(); + final int keyboardPadding = mResourceProvider.getKeyboardPadding(); + final int keyPadding = mResourceProvider.getKeyPadding(); + final float keyRadius = mResourceProvider.getKeyRadius(); + mKeyboardBackground.set(0, 0, width, height); + width -= keyboardPadding * 2; + height -= keyboardPadding * 2; + if (width <= 0 || height <= 0) { + Slog.e(TAG, "Invalid width and height to draw layout preview, width = " + width + + ", height = " + height); + return; + } + int rowCount = keys.length; + float keyHeight = (float) (height - rowCount * 2 * keyPadding) / rowCount; + float isoEnterKeyLeft = 0; + float isoEnterKeyTop = 0; + float isoEnterWidthUnit = 0; + for (int i = 0; i < rowCount; i++) { + PhysicalKeyLayout.LayoutKey[] row = keys[i]; + float totalRowWeight = 0; + int keysInRow = row.length; + for (PhysicalKeyLayout.LayoutKey layoutKey : row) { + totalRowWeight += layoutKey.keyWeight(); + } + float keyWidthInPx = (width - keysInRow * 2 * keyPadding) / totalRowWeight; + float rowWeightOnLeft = 0; + float top = keyboardPadding + keyPadding * (2 * i + 1) + i * keyHeight; + for (int j = 0; j < keysInRow; j++) { + float left = + keyboardPadding + keyPadding * (2 * j + 1) + rowWeightOnLeft * keyWidthInPx; + rowWeightOnLeft += row[j].keyWeight(); + RectF keyRect = new RectF(left, top, left + keyWidthInPx * row[j].keyWeight(), + top + keyHeight); + if (enterKey != null && row[j].keyCode() == KeyEvent.KEYCODE_ENTER) { + if (enterKey.row() == i && enterKey.column() == j) { + isoEnterKeyLeft = keyRect.left; + isoEnterKeyTop = keyRect.top; + isoEnterWidthUnit = keyWidthInPx; + } + continue; + } + if (PhysicalKeyLayout.isSpecialKey(row[j])) { + mKeyDrawables.add(new TypingKey(null, keyRect, keyRadius, + mResourceProvider.getSpecialKeyPaint(), + mResourceProvider.getSpecialKeyPaint(), + mResourceProvider.getSpecialKeyPaint())); + } else if (PhysicalKeyLayout.isKeyPositionUnsure(row[j])) { + mKeyDrawables.add(new UnsureTypingKey(row[j].glyph(), keyRect, + keyRadius, mResourceProvider.getTypingKeyPaint(), + mResourceProvider.getPrimaryGlyphPaint(), + mResourceProvider.getSecondaryGlyphPaint())); + } else { + mKeyDrawables.add(new TypingKey(row[j].glyph(), keyRect, keyRadius, + mResourceProvider.getTypingKeyPaint(), + mResourceProvider.getPrimaryGlyphPaint(), + mResourceProvider.getSecondaryGlyphPaint())); + } + } + } + if (enterKey != null) { + IsoEnterKey.Builder isoEnterKeyBuilder = new IsoEnterKey.Builder(keyRadius, + mResourceProvider.getSpecialKeyPaint()); + isoEnterKeyBuilder.setTopWidth(enterKey.topKeyWeight() * isoEnterWidthUnit) + .setStartPoint(isoEnterKeyLeft, isoEnterKeyTop) + .setVerticalEdges(keyHeight, 2 * (keyHeight + keyPadding)) + .setBottomWidth(enterKey.bottomKeyWeight() * isoEnterWidthUnit); + mKeyDrawables.add(isoEnterKeyBuilder.build()); + } + } + + @Override + public void draw(Canvas canvas) { + final float keyboardRadius = mResourceProvider.getBackgroundRadius(); + canvas.drawRoundRect(mKeyboardBackground, keyboardRadius, keyboardRadius, + mResourceProvider.getBackgroundPaint()); + for (KeyDrawable key : mKeyDrawables) { + key.draw(canvas); + } + } + + @Override + public void setAlpha(int alpha) { + // Do nothing + } + + @Override + public void setColorFilter(@Nullable ColorFilter colorFilter) { + // Do nothing + } + + @Override + public int getOpacity() { + return PixelFormat.OPAQUE; + } + + private static class TypingKey implements KeyDrawable { + + private final RectF mKeyRect; + private final float mKeyRadius; + private final Paint mKeyPaint; + private final Paint mBaseTextPaint; + private final Paint mModifierTextPaint; + private final List<GlyphDrawable> mGlyphDrawables = new ArrayList<>(); + + private TypingKey(@Nullable PhysicalKeyLayout.KeyGlyph glyphData, RectF keyRect, + float keyRadius, Paint keyPaint, Paint baseTextPaint, Paint modifierTextPaint) { + mKeyRect = keyRect; + mKeyRadius = keyRadius; + mKeyPaint = keyPaint; + mBaseTextPaint = baseTextPaint; + mModifierTextPaint = modifierTextPaint; + initGlyphs(glyphData); + } + + private void initGlyphs(@Nullable PhysicalKeyLayout.KeyGlyph glyphData) { + createGlyphs(glyphData); + measureGlyphs(); + } + + private void createGlyphs(@Nullable PhysicalKeyLayout.KeyGlyph glyphData) { + if (glyphData == null) { + return; + } + if (!glyphData.hasBaseText()) { + return; + } + if (glyphData.hasValidShiftText() && glyphData.hasValidAltGrText()) { + mGlyphDrawables.add(new GlyphDrawable(glyphData.getBaseText(), new RectF(), + GRAVITY_BOTTOM | GRAVITY_LEFT, mBaseTextPaint)); + mGlyphDrawables.add(new GlyphDrawable(glyphData.getShiftText(), new RectF(), + GRAVITY_TOP | GRAVITY_LEFT, mModifierTextPaint)); + mGlyphDrawables.add(new GlyphDrawable(glyphData.getAltGrText(), new RectF(), + GRAVITY_BOTTOM | GRAVITY_RIGHT, mModifierTextPaint)); + } else if (glyphData.hasValidShiftText()) { + mGlyphDrawables.add(new GlyphDrawable(glyphData.getBaseText(), new RectF(), + GRAVITY_BOTTOM | GRAVITY_CENTER_HORIZONTAL, mBaseTextPaint)); + mGlyphDrawables.add(new GlyphDrawable(glyphData.getShiftText(), new RectF(), + GRAVITY_TOP | GRAVITY_CENTER_HORIZONTAL, mModifierTextPaint)); + } else if (glyphData.hasValidAltGrText()) { + mGlyphDrawables.add(new GlyphDrawable(glyphData.getBaseText(), new RectF(), + GRAVITY_BOTTOM | GRAVITY_LEFT, mBaseTextPaint)); + mGlyphDrawables.add(new GlyphDrawable(glyphData.getAltGrText(), new RectF(), + GRAVITY_BOTTOM | GRAVITY_RIGHT, mModifierTextPaint)); + } else { + mGlyphDrawables.add(new GlyphDrawable(glyphData.getBaseText(), new RectF(), + GRAVITY_CENTER, mBaseTextPaint)); + } + } + + private void measureGlyphs() { + float keyWidth = mKeyRect.width(); + float keyHeight = mKeyRect.height(); + for (GlyphDrawable glyph : mGlyphDrawables) { + float centerX = keyWidth / 2; + float centerY = keyHeight / 2; + if ((glyph.gravity & GRAVITY_LEFT) != 0) { + centerX -= keyWidth / 4; + } + if ((glyph.gravity & GRAVITY_RIGHT) != 0) { + centerX += keyWidth / 4; + } + if ((glyph.gravity & GRAVITY_TOP) != 0) { + centerY -= keyHeight / 4; + } + if ((glyph.gravity & GRAVITY_BOTTOM) != 0) { + centerY += keyHeight / 4; + } + Rect textBounds = new Rect(); + glyph.paint.getTextBounds(glyph.text, 0, glyph.text.length(), textBounds); + float textWidth = textBounds.width(); + float textHeight = textBounds.height(); + glyph.rect.set(centerX - textWidth / 2, centerY - textHeight / 2 - textBounds.top, + centerX + textWidth / 2, centerY + textHeight / 2 - textBounds.top); + } + } + + @Override + public void draw(Canvas canvas) { + canvas.drawRoundRect(mKeyRect, mKeyRadius, mKeyRadius, mKeyPaint); + for (GlyphDrawable glyph : mGlyphDrawables) { + float textWidth = glyph.rect.width(); + float textHeight = glyph.rect.height(); + float keyWidth = mKeyRect.width(); + float keyHeight = mKeyRect.height(); + if (textWidth == 0 || textHeight == 0 || keyWidth == 0 || keyHeight == 0) { + return; + } + canvas.drawText(glyph.text, 0, glyph.text.length(), mKeyRect.left + glyph.rect.left, + mKeyRect.top + glyph.rect.top, glyph.paint); + } + } + } + + private static class UnsureTypingKey extends TypingKey { + + private UnsureTypingKey(@Nullable PhysicalKeyLayout.KeyGlyph glyphData, + RectF keyRect, float keyRadius, Paint keyPaint, Paint baseTextPaint, + Paint modifierTextPaint) { + super(glyphData, keyRect, keyRadius, createGreyedOutPaint(keyPaint), + createGreyedOutPaint(baseTextPaint), createGreyedOutPaint(modifierTextPaint)); + } + } + + private static class IsoEnterKey implements KeyDrawable { + + private final Paint mKeyPaint; + private final Path mPath; + + private IsoEnterKey(Paint keyPaint, @NonNull Path path) { + mKeyPaint = keyPaint; + mPath = path; + } + + @Override + public void draw(Canvas canvas) { + canvas.drawPath(mPath, mKeyPaint); + } + + private static class Builder { + private final float mKeyRadius; + private final Paint mKeyPaint; + private float mLeft; + private float mTop; + private float mTopWidth; + private float mBottomWidth; + private float mLeftHeight; + private float mRightHeight; + + private Builder(float keyRadius, Paint keyPaint) { + mKeyRadius = keyRadius; + mKeyPaint = keyPaint; + } + + private Builder setStartPoint(float left, float top) { + mLeft = left; + mTop = top; + return this; + } + + private Builder setTopWidth(float width) { + mTopWidth = width; + return this; + } + + private Builder setBottomWidth(float width) { + mBottomWidth = width; + return this; + } + + private Builder setVerticalEdges(float leftHeight, float rightHeight) { + mLeftHeight = leftHeight; + mRightHeight = rightHeight; + return this; + } + + private IsoEnterKey build() { + Path enterKey = new Path(); + RectF oval = new RectF(-mKeyRadius, -mKeyRadius, mKeyRadius, mKeyRadius); + // Horizontal top line + enterKey.moveTo(mLeft + mKeyRadius, mTop); + enterKey.lineTo(mLeft + mTopWidth - mKeyRadius, mTop); + // Rounded top right corner + oval.offsetTo(mLeft + mTopWidth - 2 * mKeyRadius, mTop); + enterKey.arcTo(oval, 270, 90); + // Vertical right line + enterKey.lineTo(mLeft + mTopWidth, mTop + mRightHeight - mKeyRadius); + // Rounded bottom right corner + oval.offsetTo(mLeft + mTopWidth - 2 * mKeyRadius, + mTop + mRightHeight - 2 * mKeyRadius); + enterKey.arcTo(oval, 0, 90); + // Horizontal bottom line + enterKey.lineTo(mLeft + mTopWidth - mBottomWidth + mKeyRadius, mTop + mRightHeight); + // Rounded bottom left corner + oval.offsetTo(mLeft + mTopWidth - mBottomWidth, + mTop + mRightHeight - 2 * mKeyRadius); + enterKey.arcTo(oval, 90, 90); + // Vertical left line (bottom half) + enterKey.lineTo(mLeft + mTopWidth - mBottomWidth, mTop + mLeftHeight - mKeyRadius); + // Rounded corner + oval.offsetTo(mLeft + mTopWidth - mBottomWidth - 2 * mKeyRadius, + mTop + mLeftHeight); + enterKey.arcTo(oval, 0, -90); + // Horizontal line in the mid part + enterKey.lineTo(mLeft + mKeyRadius, mTop + mLeftHeight); + // Rounded corner + oval.offsetTo(mLeft, mTop + mLeftHeight - 2 * mKeyRadius); + enterKey.arcTo(oval, 90, 90); + // Vertical left line (top half) + enterKey.lineTo(mLeft, mTop + mKeyRadius); + // Rounded top left corner + oval.offsetTo(mLeft, mTop); + enterKey.arcTo(oval, 180, 90); + enterKey.close(); + return new IsoEnterKey(mKeyPaint, enterKey); + } + } + } + + private record GlyphDrawable(String text, RectF rect, int gravity, Paint paint) {} + + private interface KeyDrawable { + void draw(Canvas canvas); + } + + private static class ResourceProvider { + // Resources + private final Paint mBackgroundPaint; + private final Paint mTypingKeyPaint; + private final Paint mSpecialKeyPaint; + private final Paint mPrimaryGlyphPaint; + private final Paint mSecondaryGlyphPaint; + private final int mKeyPadding; + private final int mKeyboardPadding; + private final float mKeyRadius; + private final float mBackgroundRadius; + + private ResourceProvider(Context context) { + mKeyPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + KEY_PADDING_IN_DP, context.getResources().getDisplayMetrics()); + mKeyboardPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + KEYBOARD_PADDING_IN_DP, context.getResources().getDisplayMetrics()); + mKeyRadius = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + KEY_RADIUS_IN_DP, context.getResources().getDisplayMetrics()); + mBackgroundRadius = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + KEYBOARD_RADIUS_IN_DP, context.getResources().getDisplayMetrics()); + int textSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, + GLYPH_TEXT_SIZE_IN_SP, context.getResources().getDisplayMetrics()); + boolean isDark = (context.getResources().getConfiguration().uiMode + & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; + int typingKeyColor = context.getColor( + isDark ? android.R.color.system_outline_variant_dark + : android.R.color.system_surface_container_lowest_light); + int specialKeyColor = context.getColor(isDark ? android.R.color.system_neutral1_800 + : android.R.color.system_secondary_container_light); + int primaryGlyphColor = context.getColor(isDark ? android.R.color.system_on_surface_dark + : android.R.color.system_on_surface_light); + int secondaryGlyphColor = context.getColor(isDark ? android.R.color.system_outline_dark + : android.R.color.system_outline_light); + int backgroundColor = context.getColor( + isDark ? android.R.color.system_surface_container_dark + : android.R.color.system_surface_container_light); + mPrimaryGlyphPaint = createTextPaint(primaryGlyphColor, textSize, + Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)); + mSecondaryGlyphPaint = createTextPaint(secondaryGlyphColor, textSize, + Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL)); + mTypingKeyPaint = createFillPaint(typingKeyColor); + mSpecialKeyPaint = createFillPaint(specialKeyColor); + mBackgroundPaint = createFillPaint(backgroundColor); + } + + private Paint getBackgroundPaint() { + return mBackgroundPaint; + } + + private Paint getTypingKeyPaint() { + return mTypingKeyPaint; + } + + private Paint getSpecialKeyPaint() { + return mSpecialKeyPaint; + } + + private Paint getPrimaryGlyphPaint() { + return mPrimaryGlyphPaint; + } + + private Paint getSecondaryGlyphPaint() { + return mSecondaryGlyphPaint; + } + + private int getKeyPadding() { + return mKeyPadding; + } + + private int getKeyboardPadding() { + return mKeyboardPadding; + } + + private float getKeyRadius() { + return mKeyRadius; + } + + private float getBackgroundRadius() { + return mBackgroundRadius; + } + } + + private static Paint createTextPaint(@ColorInt int textColor, int textSize, Typeface typeface) { + Paint paint = new Paint(); + paint.setColor(textColor); + paint.setStyle(Paint.Style.FILL); + paint.setTextSize(textSize); + paint.setTypeface(typeface); + return paint; + } + + private static Paint createFillPaint(@ColorInt int color) { + Paint paint = new Paint(); + paint.setColor(color); + paint.setStyle(Paint.Style.FILL); + return paint; + } + + private static Paint createGreyedOutPaint(Paint paint) { + Paint result = new Paint(paint); + result.setAlpha(100); + return result; + } +} diff --git a/core/java/android/hardware/input/PhysicalKeyLayout.java b/core/java/android/hardware/input/PhysicalKeyLayout.java new file mode 100644 index 000000000000..241c452a75eb --- /dev/null +++ b/core/java/android/hardware/input/PhysicalKeyLayout.java @@ -0,0 +1,434 @@ +/* + * Copyright 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 android.annotation.NonNull; +import android.annotation.Nullable; +import android.text.TextUtils; +import android.util.SparseIntArray; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; + +import java.util.Locale; + +/** + * A complimentary class to {@link KeyboardLayoutPreviewDrawable} describing the physical key layout + * of a Physical keyboard and provides information regarding the scan codes produced by the physical + * keys. + */ +final class PhysicalKeyLayout { + + private static final String TAG = "KeyboardLayoutPreview"; + private static final int SCANCODE_1 = 2; + private static final int SCANCODE_2 = 3; + private static final int SCANCODE_3 = 4; + private static final int SCANCODE_4 = 5; + private static final int SCANCODE_5 = 6; + private static final int SCANCODE_6 = 7; + private static final int SCANCODE_7 = 8; + private static final int SCANCODE_8 = 9; + private static final int SCANCODE_9 = 10; + private static final int SCANCODE_0 = 11; + private static final int SCANCODE_MINUS = 12; + private static final int SCANCODE_EQUALS = 13; + private static final int SCANCODE_Q = 16; + private static final int SCANCODE_W = 17; + private static final int SCANCODE_E = 18; + private static final int SCANCODE_R = 19; + private static final int SCANCODE_T = 20; + private static final int SCANCODE_Y = 21; + private static final int SCANCODE_U = 22; + private static final int SCANCODE_I = 23; + private static final int SCANCODE_O = 24; + private static final int SCANCODE_P = 25; + private static final int SCANCODE_LEFT_BRACKET = 26; + private static final int SCANCODE_RIGHT_BRACKET = 27; + private static final int SCANCODE_A = 30; + private static final int SCANCODE_S = 31; + private static final int SCANCODE_D = 32; + private static final int SCANCODE_F = 33; + private static final int SCANCODE_G = 34; + private static final int SCANCODE_H = 35; + private static final int SCANCODE_J = 36; + private static final int SCANCODE_K = 37; + private static final int SCANCODE_L = 38; + private static final int SCANCODE_SEMICOLON = 39; + private static final int SCANCODE_APOSTROPHE = 40; + private static final int SCANCODE_GRAVE = 41; + private static final int SCANCODE_BACKSLASH1 = 43; + private static final int SCANCODE_Z = 44; + private static final int SCANCODE_X = 45; + private static final int SCANCODE_C = 46; + private static final int SCANCODE_V = 47; + private static final int SCANCODE_B = 48; + private static final int SCANCODE_N = 49; + private static final int SCANCODE_M = 50; + private static final int SCANCODE_COMMA = 51; + private static final int SCANCODE_PERIOD = 52; + private static final int SCANCODE_SLASH = 53; + private static final int SCANCODE_BACKSLASH2 = 86; + private static final int SCANCODE_YEN = 124; + + private static final SparseIntArray DEFAULT_KEYCODE_FOR_SCANCODE = new SparseIntArray(); + + static { + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_1, KeyEvent.KEYCODE_1); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_2, KeyEvent.KEYCODE_2); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_3, KeyEvent.KEYCODE_3); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_4, KeyEvent.KEYCODE_4); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_5, KeyEvent.KEYCODE_5); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_6, KeyEvent.KEYCODE_6); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_7, KeyEvent.KEYCODE_7); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_8, KeyEvent.KEYCODE_8); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_9, KeyEvent.KEYCODE_9); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_0, KeyEvent.KEYCODE_0); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_MINUS, KeyEvent.KEYCODE_MINUS); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_EQUALS, KeyEvent.KEYCODE_EQUALS); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_Q, KeyEvent.KEYCODE_Q); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_W, KeyEvent.KEYCODE_W); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_E, KeyEvent.KEYCODE_E); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_R, KeyEvent.KEYCODE_R); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_T, KeyEvent.KEYCODE_T); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_Y, KeyEvent.KEYCODE_Y); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_U, KeyEvent.KEYCODE_U); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_I, KeyEvent.KEYCODE_I); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_O, KeyEvent.KEYCODE_O); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_P, KeyEvent.KEYCODE_P); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_LEFT_BRACKET, KeyEvent.KEYCODE_LEFT_BRACKET); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_RIGHT_BRACKET, KeyEvent.KEYCODE_RIGHT_BRACKET); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_A, KeyEvent.KEYCODE_A); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_S, KeyEvent.KEYCODE_S); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_D, KeyEvent.KEYCODE_D); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_F, KeyEvent.KEYCODE_F); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_G, KeyEvent.KEYCODE_G); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_H, KeyEvent.KEYCODE_H); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_J, KeyEvent.KEYCODE_J); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_K, KeyEvent.KEYCODE_K); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_L, KeyEvent.KEYCODE_L); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_SEMICOLON, KeyEvent.KEYCODE_SEMICOLON); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_APOSTROPHE, KeyEvent.KEYCODE_APOSTROPHE); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_GRAVE, KeyEvent.KEYCODE_GRAVE); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_BACKSLASH1, KeyEvent.KEYCODE_BACKSLASH); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_Z, KeyEvent.KEYCODE_Z); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_X, KeyEvent.KEYCODE_X); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_C, KeyEvent.KEYCODE_C); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_V, KeyEvent.KEYCODE_V); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_B, KeyEvent.KEYCODE_B); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_N, KeyEvent.KEYCODE_N); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_M, KeyEvent.KEYCODE_M); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_COMMA, KeyEvent.KEYCODE_COMMA); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_PERIOD, KeyEvent.KEYCODE_PERIOD); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_SLASH, KeyEvent.KEYCODE_SLASH); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_BACKSLASH2, KeyEvent.KEYCODE_BACKSLASH); + DEFAULT_KEYCODE_FOR_SCANCODE.put(SCANCODE_YEN, KeyEvent.KEYCODE_YEN); + } + + private LayoutKey[][] mKeys = null; + private EnterKey mEnterKey = null; + + public PhysicalKeyLayout(@NonNull KeyCharacterMap kcm, @Nullable KeyboardLayout layout) { + initLayoutKeys(kcm, layout); + } + + private void initLayoutKeys(KeyCharacterMap kcm, KeyboardLayout layout) { + if (layout == null) { + createIsoLayout(kcm); + return; + } + if (layout.isAnsiLayout()) { + createAnsiLayout(kcm); + } else if (layout.isJisLayout()) { + createJisLayout(kcm); + } else { + createIsoLayout(kcm); + } + } + + public LayoutKey[][] getKeys() { + return mKeys; + } + + /** + * @return Special enter key (if required) that can span multiple rows like ISO enter key. + */ + @Nullable + public EnterKey getEnterKey() { + return mEnterKey; + } + + private void createAnsiLayout(KeyCharacterMap kcm) { + mKeys = new LayoutKey[][]{ + { + getKey(kcm, SCANCODE_GRAVE), getKey(kcm, SCANCODE_1), + getKey(kcm, SCANCODE_2), getKey(kcm, SCANCODE_3), getKey(kcm, SCANCODE_4), + getKey(kcm, SCANCODE_5), getKey(kcm, SCANCODE_6), getKey(kcm, SCANCODE_7), + getKey(kcm, SCANCODE_8), getKey(kcm, SCANCODE_9), getKey(kcm, SCANCODE_0), + getKey(kcm, SCANCODE_MINUS), getKey(kcm, SCANCODE_EQUALS), + getKey(KeyEvent.KEYCODE_DEL, 1.5F) + }, + { + getKey(KeyEvent.KEYCODE_TAB, 1.5F), getKey(kcm, SCANCODE_Q), + getKey(kcm, SCANCODE_W), getKey(kcm, SCANCODE_E), getKey(kcm, SCANCODE_R), + getKey(kcm, SCANCODE_T), getKey(kcm, SCANCODE_Y), getKey(kcm, SCANCODE_U), + getKey(kcm, SCANCODE_I), getKey(kcm, SCANCODE_O), getKey(kcm, SCANCODE_P), + getKey(kcm, SCANCODE_LEFT_BRACKET), getKey(kcm, SCANCODE_RIGHT_BRACKET), + getKey(kcm, SCANCODE_BACKSLASH1) + }, + { + getKey(KeyEvent.KEYCODE_CAPS_LOCK, 1.75F), + getKey(kcm, SCANCODE_A), getKey(kcm, SCANCODE_S), getKey(kcm, SCANCODE_D), + getKey(kcm, SCANCODE_F), getKey(kcm, SCANCODE_G), getKey(kcm, SCANCODE_H), + getKey(kcm, SCANCODE_J), getKey(kcm, SCANCODE_K), getKey(kcm, SCANCODE_L), + getKey(kcm, SCANCODE_SEMICOLON), getKey(kcm, SCANCODE_APOSTROPHE), + getKey(KeyEvent.KEYCODE_ENTER, 1.75F) + }, + { + getKey(KeyEvent.KEYCODE_SHIFT_LEFT, 2.5F), + getKey(kcm, SCANCODE_Z), getKey(kcm, SCANCODE_X), getKey(kcm, SCANCODE_C), + getKey(kcm, SCANCODE_V), getKey(kcm, SCANCODE_B), getKey(kcm, SCANCODE_N), + getKey(kcm, SCANCODE_M), getKey(kcm, SCANCODE_COMMA), + getKey(kcm, SCANCODE_PERIOD), getKey(kcm, SCANCODE_SLASH), + getKey(KeyEvent.KEYCODE_SHIFT_RIGHT, 2.5F), + }, + { + getKey(KeyEvent.KEYCODE_CTRL_LEFT, 1.0F), + getKey(KeyEvent.KEYCODE_FUNCTION, 1.0F), + getKey(KeyEvent.KEYCODE_META_LEFT, 1.0F), + getKey(KeyEvent.KEYCODE_ALT_LEFT, 1.0F), + getKey(KeyEvent.KEYCODE_SPACE, 6.5F), + getKey(KeyEvent.KEYCODE_ALT_RIGHT, 1.0F), + getKey(KeyEvent.KEYCODE_META_RIGHT, 1.0F), + getKey(KeyEvent.KEYCODE_MENU, 1.0F), + getKey(KeyEvent.KEYCODE_CTRL_RIGHT, 1.0F), + } + }; + } + + private void createIsoLayout(KeyCharacterMap kcm) { + mKeys = new LayoutKey[][]{ + { + getKey(kcm, SCANCODE_GRAVE), getKey(kcm, SCANCODE_1), + getKey(kcm, SCANCODE_2), getKey(kcm, SCANCODE_3), getKey(kcm, SCANCODE_4), + getKey(kcm, SCANCODE_5), getKey(kcm, SCANCODE_6), getKey(kcm, SCANCODE_7), + getKey(kcm, SCANCODE_8), getKey(kcm, SCANCODE_9), getKey(kcm, SCANCODE_0), + getKey(kcm, SCANCODE_MINUS), getKey(kcm, SCANCODE_EQUALS), + getKey(KeyEvent.KEYCODE_DEL, 1.5F) + }, + { + getKey(KeyEvent.KEYCODE_TAB, 1.15F), getKey(kcm, SCANCODE_Q), + getKey(kcm, SCANCODE_W), getKey(kcm, SCANCODE_E), getKey(kcm, SCANCODE_R), + getKey(kcm, SCANCODE_T), getKey(kcm, SCANCODE_Y), getKey(kcm, SCANCODE_U), + getKey(kcm, SCANCODE_I), getKey(kcm, SCANCODE_O), getKey(kcm, SCANCODE_P), + getKey(kcm, SCANCODE_LEFT_BRACKET), getKey(kcm, SCANCODE_RIGHT_BRACKET), + getKey(KeyEvent.KEYCODE_ENTER, 1.35F) + }, + { + getKey(KeyEvent.KEYCODE_TAB, 1.5F), getKey(kcm, SCANCODE_A), + getKey(kcm, SCANCODE_S), getKey(kcm, SCANCODE_D), getKey(kcm, SCANCODE_F), + getKey(kcm, SCANCODE_G), getKey(kcm, SCANCODE_H), getKey(kcm, SCANCODE_J), + getKey(kcm, SCANCODE_K), getKey(kcm, SCANCODE_L), + getKey(kcm, SCANCODE_SEMICOLON), getKey(kcm, SCANCODE_APOSTROPHE), + getKey(kcm, SCANCODE_BACKSLASH1), + getKey(KeyEvent.KEYCODE_ENTER, 1.0F) + }, + { + getKey(KeyEvent.KEYCODE_SHIFT_LEFT, 1.15F), + getKey(kcm, SCANCODE_BACKSLASH2), getKey(kcm, SCANCODE_Z), + getKey(kcm, SCANCODE_X), getKey(kcm, SCANCODE_C), getKey(kcm, SCANCODE_V), + getKey(kcm, SCANCODE_B), getKey(kcm, SCANCODE_N), getKey(kcm, SCANCODE_M), + getKey(kcm, SCANCODE_COMMA), getKey(kcm, SCANCODE_PERIOD), + getKey(kcm, SCANCODE_SLASH), + getKey(KeyEvent.KEYCODE_SHIFT_RIGHT, 2.35F) + }, + { + getKey(KeyEvent.KEYCODE_CTRL_LEFT, 1.0F), + getKey(KeyEvent.KEYCODE_FUNCTION, 1.0F), + getKey(KeyEvent.KEYCODE_META_LEFT, 1.0F), + getKey(KeyEvent.KEYCODE_ALT_LEFT, 1.0F), + getKey(KeyEvent.KEYCODE_SPACE, 6.5F), + getKey(KeyEvent.KEYCODE_ALT_RIGHT, 1.0F), + getKey(KeyEvent.KEYCODE_META_RIGHT, 1.0F), + getKey(KeyEvent.KEYCODE_MENU, 1.0F), + getKey(KeyEvent.KEYCODE_CTRL_RIGHT, 1.0F), + } + }; + mEnterKey = new EnterKey(1, 13, 1.35F, 1.0F); + } + + private void createJisLayout(KeyCharacterMap kcm) { + mKeys = new LayoutKey[][]{ + { + getKey(kcm, SCANCODE_GRAVE), getKey(kcm, SCANCODE_1), + getKey(kcm, SCANCODE_2), getKey(kcm, SCANCODE_3), getKey(kcm, SCANCODE_4), + getKey(kcm, SCANCODE_5), getKey(kcm, SCANCODE_6), getKey(kcm, SCANCODE_7), + getKey(kcm, SCANCODE_8), getKey(kcm, SCANCODE_9), getKey(kcm, SCANCODE_0), + getKey(kcm, SCANCODE_MINUS, 0.8F), getKey(kcm, SCANCODE_EQUALS, 0.8f), + getKey(kcm, SCANCODE_YEN, 0.8f), getKey(KeyEvent.KEYCODE_DEL, 1.1F) + }, + { + getKey(KeyEvent.KEYCODE_TAB, 1.15F), getKey(kcm, SCANCODE_Q), + getKey(kcm, SCANCODE_W), getKey(kcm, SCANCODE_E), getKey(kcm, SCANCODE_R), + getKey(kcm, SCANCODE_T), getKey(kcm, SCANCODE_Y), getKey(kcm, SCANCODE_U), + getKey(kcm, SCANCODE_I), getKey(kcm, SCANCODE_O), getKey(kcm, SCANCODE_P), + getKey(kcm, SCANCODE_LEFT_BRACKET), getKey(kcm, SCANCODE_RIGHT_BRACKET), + getKey(KeyEvent.KEYCODE_ENTER, 1.35F) + }, + { + getKey(KeyEvent.KEYCODE_TAB, 1.5F), getKey(kcm, SCANCODE_A), + getKey(kcm, SCANCODE_S), getKey(kcm, SCANCODE_D), getKey(kcm, SCANCODE_F), + getKey(kcm, SCANCODE_G), getKey(kcm, SCANCODE_H), getKey(kcm, SCANCODE_J), + getKey(kcm, SCANCODE_K), getKey(kcm, SCANCODE_L), + getKey(kcm, SCANCODE_SEMICOLON), getKey(kcm, SCANCODE_APOSTROPHE), + getKey(kcm, SCANCODE_BACKSLASH2), + getKey(KeyEvent.KEYCODE_ENTER, 1.0F) + }, + { + getKey(KeyEvent.KEYCODE_SHIFT_LEFT, 1.15F), + getKey(kcm, SCANCODE_Z), getKey(kcm, SCANCODE_X), getKey(kcm, SCANCODE_C), + getKey(kcm, SCANCODE_V), getKey(kcm, SCANCODE_B), getKey(kcm, SCANCODE_N), + getKey(kcm, SCANCODE_M), getKey(kcm, SCANCODE_COMMA), + getKey(kcm, SCANCODE_PERIOD), getKey(kcm, SCANCODE_SLASH), + getKey(kcm, SCANCODE_BACKSLASH1), + getKey(KeyEvent.KEYCODE_SHIFT_RIGHT, 2.35F) + }, + { + getKey(KeyEvent.KEYCODE_CTRL_LEFT, 1.0F), + getKey(KeyEvent.KEYCODE_FUNCTION, 1.0F), + getKey(KeyEvent.KEYCODE_META_LEFT, 1.0F), + getKey(KeyEvent.KEYCODE_ALT_LEFT, 1.0F), + getKey(KeyEvent.KEYCODE_UNKNOWN, 1.0F), + getKey(KeyEvent.KEYCODE_SPACE, 3.5F), + getKey(KeyEvent.KEYCODE_UNKNOWN, 1.0F), + getKey(KeyEvent.KEYCODE_UNKNOWN, 1.0F), + getKey(KeyEvent.KEYCODE_ALT_RIGHT, 1.0F), + getKey(KeyEvent.KEYCODE_META_RIGHT, 1.0F), + getKey(KeyEvent.KEYCODE_MENU, 1.0F), + getKey(KeyEvent.KEYCODE_CTRL_RIGHT, 1.0F), + } + }; + mEnterKey = new EnterKey(1, 13, 1.35F, 1.0F); + } + + private static LayoutKey getKey(KeyCharacterMap kcm, int scanCode, float keyWeight) { + int keyCode = kcm.getMappedKeyOrDefault(scanCode, + DEFAULT_KEYCODE_FOR_SCANCODE.get(scanCode, KeyEvent.KEYCODE_UNKNOWN)); + return new LayoutKey(keyCode, scanCode, keyWeight, new KeyGlyph(kcm, keyCode)); + } + + private static LayoutKey getKey(KeyCharacterMap kcm, int scanCode) { + return getKey(kcm, scanCode, 1.0F); + } + + private static String getKeyText(KeyCharacterMap kcm, int keyCode, int modifierState) { + if (isSpecialKey(keyCode)) { + return ""; + } + int utf8Char = (kcm.get(keyCode, modifierState) & KeyCharacterMap.COMBINING_ACCENT_MASK); + if (Character.isValidCodePoint(utf8Char)) { + return String.valueOf(Character.toChars(utf8Char)).toUpperCase(Locale.getDefault()); + } else { + return String.valueOf(kcm.getDisplayLabel(keyCode)).toUpperCase(Locale.getDefault()); + } + } + + private static LayoutKey getKey(int keyCode, float keyWeight) { + return new LayoutKey(keyCode, keyCode, keyWeight, null); + } + + /** + * Util function that tells if a key corresponds to a special key which are keys on a Physical + * layout that perform some special action like modifier keys, enter key, space key, character + * set changing keys, etc. + */ + private static boolean isSpecialKey(int keyCode) { + switch (keyCode) { + case KeyEvent.KEYCODE_DEL: + case KeyEvent.KEYCODE_TAB: + case KeyEvent.KEYCODE_CAPS_LOCK: + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_SHIFT_LEFT: + case KeyEvent.KEYCODE_SHIFT_RIGHT: + case KeyEvent.KEYCODE_CTRL_LEFT: + case KeyEvent.KEYCODE_CTRL_RIGHT: + case KeyEvent.KEYCODE_FUNCTION: + case KeyEvent.KEYCODE_ALT_LEFT: + case KeyEvent.KEYCODE_ALT_RIGHT: + case KeyEvent.KEYCODE_META_LEFT: + case KeyEvent.KEYCODE_META_RIGHT: + case KeyEvent.KEYCODE_SPACE: + case KeyEvent.KEYCODE_MENU: + case KeyEvent.KEYCODE_UNKNOWN: + return true; + } + return false; + } + + public static boolean isSpecialKey(LayoutKey key) { + return isSpecialKey(key.keyCode); + } + + public static boolean isKeyPositionUnsure(LayoutKey key) { + switch (key.scanCode) { + case SCANCODE_GRAVE: + case SCANCODE_BACKSLASH1: + case SCANCODE_BACKSLASH2: + return true; + } + return false; + } + + public record LayoutKey(int keyCode, int scanCode, float keyWeight, KeyGlyph glyph) {} + public record EnterKey(int row, int column, float topKeyWeight, float bottomKeyWeight) {} + + public static class KeyGlyph { + private final String mBaseText; + private final String mShiftText; + private final String mAltGrText; + + public KeyGlyph(KeyCharacterMap kcm, int keyCode) { + mBaseText = getKeyText(kcm, keyCode, 0); + mShiftText = getKeyText(kcm, keyCode, + KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_LEFT_ON); + mAltGrText = getKeyText(kcm, keyCode, + KeyEvent.META_ALT_ON | KeyEvent.META_ALT_RIGHT_ON); + } + + public String getBaseText() { + return mBaseText; + } + + public String getShiftText() { + return mShiftText; + } + + public String getAltGrText() { + return mAltGrText; + } + + public boolean hasBaseText() { + return !TextUtils.isEmpty(mBaseText); + } + + public boolean hasValidShiftText() { + return !TextUtils.isEmpty(mShiftText) && !TextUtils.equals(mBaseText, mShiftText); + } + + public boolean hasValidAltGrText() { + return !TextUtils.isEmpty(mAltGrText) && !TextUtils.equals(mBaseText, mAltGrText); + } + } +} diff --git a/core/java/android/view/KeyCharacterMap.aidl b/core/java/android/view/KeyCharacterMap.aidl new file mode 100644 index 000000000000..1a761a67b520 --- /dev/null +++ b/core/java/android/view/KeyCharacterMap.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 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.view; + +parcelable KeyCharacterMap;
\ No newline at end of file diff --git a/core/java/android/view/KeyCharacterMap.java b/core/java/android/view/KeyCharacterMap.java index d8221a6267fd..4fe53c2410f5 100644 --- a/core/java/android/view/KeyCharacterMap.java +++ b/core/java/android/view/KeyCharacterMap.java @@ -16,6 +16,7 @@ package android.view; +import android.annotation.NonNull; import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; import android.hardware.input.InputManagerGlobal; @@ -309,6 +310,10 @@ public class KeyCharacterMap implements Parcelable { private static native KeyCharacterMap nativeObtainEmptyKeyCharacterMap(int deviceId); private static native boolean nativeEquals(long ptr1, long ptr2); + private static native void nativeApplyOverlay(long ptr, String layoutDescriptor, + String overlay); + private static native int nativeGetMappedKey(long ptr, int scanCode); + private KeyCharacterMap(Parcel in) { if (in == null) { throw new IllegalArgumentException("parcel must not be null"); @@ -368,6 +373,38 @@ public class KeyCharacterMap implements Parcelable { } /** + * Loads the key character map with applied KCM overlay. + * + * @param layoutDescriptor descriptor of the applied overlay KCM + * @param overlay string describing the overlay KCM + * @return The resultant key character map. + * @throws {@link UnavailableException} if the key character map + * could not be loaded because it was malformed or the default key character map + * is missing from the system. + * @hide + */ + public static KeyCharacterMap load(@NonNull String layoutDescriptor, @NonNull String overlay) { + KeyCharacterMap kcm = KeyCharacterMap.load(VIRTUAL_KEYBOARD); + kcm.applyOverlay(layoutDescriptor, overlay); + return kcm; + } + + private void applyOverlay(@NonNull String layoutDescriptor, @NonNull String overlay) { + nativeApplyOverlay(mPtr, layoutDescriptor, overlay); + } + + /** + * Gets the mapped key for the provided scan code. Returns the provided default if no mapping + * found in the KeyCharacterMap. + * + * @hide + */ + public int getMappedKeyOrDefault(int scanCode, int defaultKeyCode) { + int keyCode = nativeGetMappedKey(mPtr, scanCode); + return keyCode == KeyEvent.KEYCODE_UNKNOWN ? defaultKeyCode : keyCode; + } + + /** * Gets the Unicode character generated by the specified key and meta * key state combination. * <p> diff --git a/core/jni/android_view_KeyCharacterMap.cpp b/core/jni/android_view_KeyCharacterMap.cpp index ddaeb5a4d272..7f69e22fb0d1 100644 --- a/core/jni/android_view_KeyCharacterMap.cpp +++ b/core/jni/android_view_KeyCharacterMap.cpp @@ -240,6 +240,36 @@ static jboolean nativeEquals(JNIEnv* env, jobject clazz, jlong ptr1, jlong ptr2) return static_cast<jboolean>(*map1 == *map2); } +static void nativeApplyOverlay(JNIEnv* env, jobject clazz, jlong ptr, jstring nameObj, + jstring overlayObj) { + NativeKeyCharacterMap* map = reinterpret_cast<NativeKeyCharacterMap*>(ptr); + if (!map || !map->getMap()) { + return; + } + ScopedUtfChars nameChars(env, nameObj); + ScopedUtfChars overlayChars(env, overlayObj); + base::Result<std::shared_ptr<KeyCharacterMap>> ret = + KeyCharacterMap::loadContents(nameChars.c_str(), overlayChars.c_str(), + KeyCharacterMap::Format::OVERLAY); + if (ret.ok()) { + std::shared_ptr<KeyCharacterMap> overlay = *ret; + map->getMap()->combine(*overlay); + } +} + +static jint nativeGetMappedKey(JNIEnv* env, jobject clazz, jlong ptr, jint scanCode) { + NativeKeyCharacterMap* map = reinterpret_cast<NativeKeyCharacterMap*>(ptr); + if (!map || !map->getMap()) { + return 0; + } + int32_t outKeyCode; + status_t mapKeyRes = map->getMap()->mapKey(scanCode, /*usageCode=*/0, &outKeyCode); + if (mapKeyRes != OK) { + return 0; + } + return static_cast<jint>(outKeyCode); +} + /* * JNI registration. */ @@ -260,7 +290,9 @@ static const JNINativeMethod g_methods[] = { {"nativeObtainEmptyKeyCharacterMap", "(I)Landroid/view/KeyCharacterMap;", (void*)nativeObtainEmptyKeyCharacterMap}, {"nativeEquals", "(JJ)Z", (void*)nativeEquals}, -}; + {"nativeApplyOverlay", "(JLjava/lang/String;Ljava/lang/String;)V", + (void*)nativeApplyOverlay}, + {"nativeGetMappedKey", "(JI)I", (void*)nativeGetMappedKey}}; int register_android_view_KeyCharacterMap(JNIEnv* env) { diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 62660c4f3c6d..6b399def4d73 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -95,6 +95,7 @@ import android.view.InputChannel; import android.view.InputDevice; import android.view.InputEvent; import android.view.InputMonitor; +import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.PointerIcon; import android.view.Surface; @@ -682,6 +683,12 @@ public class InputManagerService extends IInputManager.Stub return mNative.getKeyCodeForKeyLocation(deviceId, locationKeyCode); } + @Override // Binder call + public KeyCharacterMap getKeyCharacterMap(@NonNull String layoutDescriptor) { + Objects.requireNonNull(layoutDescriptor, "layoutDescriptor must not be null"); + return mKeyboardLayoutManager.getKeyCharacterMap(layoutDescriptor); + } + /** * Transfer the current touch gesture to the provided window. * diff --git a/services/core/java/com/android/server/input/KeyboardLayoutManager.java b/services/core/java/com/android/server/input/KeyboardLayoutManager.java index a5162c09f838..0eb620f3f4df 100644 --- a/services/core/java/com/android/server/input/KeyboardLayoutManager.java +++ b/services/core/java/com/android/server/input/KeyboardLayoutManager.java @@ -63,6 +63,7 @@ import android.util.Log; import android.util.Slog; import android.util.SparseArray; import android.view.InputDevice; +import android.view.KeyCharacterMap; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; @@ -430,6 +431,23 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { return result[0]; } + @AnyThread + public KeyCharacterMap getKeyCharacterMap(@NonNull String layoutDescriptor) { + final String[] overlay = new String[1]; + visitKeyboardLayout(layoutDescriptor, + (resources, keyboardLayoutResId, layout) -> { + try (InputStreamReader stream = new InputStreamReader( + resources.openRawResource(keyboardLayoutResId))) { + overlay[0] = Streams.readFully(stream); + } catch (IOException | Resources.NotFoundException ignored) { + } + }); + if (TextUtils.isEmpty(overlay[0])) { + return KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); + } + return KeyCharacterMap.load(layoutDescriptor, overlay[0]); + } + private void visitAllKeyboardLayouts(KeyboardLayoutVisitor visitor) { final PackageManager pm = mContext.getPackageManager(); Intent intent = new Intent(InputManager.ACTION_QUERY_KEYBOARD_LAYOUTS); diff --git a/tests/Input/Android.bp b/tests/Input/Android.bp index 96b685dc356a..365e00e2b652 100644 --- a/tests/Input/Android.bp +++ b/tests/Input/Android.bp @@ -26,6 +26,7 @@ android_test { "androidx.test.runner", "androidx.test.uiautomator_uiautomator", "servicestests-utils", + "flag-junit", "frameworks-base-testutils", "hamcrest-library", "kotlin-test", diff --git a/tests/Input/src/android/hardware/input/KeyboardLayoutPreviewTests.kt b/tests/Input/src/android/hardware/input/KeyboardLayoutPreviewTests.kt new file mode 100644 index 000000000000..3a2a3be0690d --- /dev/null +++ b/tests/Input/src/android/hardware/input/KeyboardLayoutPreviewTests.kt @@ -0,0 +1,69 @@ +/* + * Copyright 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 android.content.ContextWrapper +import android.graphics.drawable.Drawable +import android.platform.test.annotations.Presubmit +import android.platform.test.flag.junit.SetFlagsRule +import androidx.test.platform.app.InstrumentationRegistry +import com.android.hardware.input.Flags +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.junit.MockitoJUnitRunner + +/** + * Tests for Keyboard layout preview + * + * Build/Install/Run: + * atest InputTests:KeyboardLayoutPreviewTests + */ +@Presubmit +@RunWith(MockitoJUnitRunner::class) +class KeyboardLayoutPreviewTests { + + companion object { + const val WIDTH = 100 + const val HEIGHT = 100 + } + + @get:Rule + val setFlagsRule = SetFlagsRule() + + private fun createDrawable(): Drawable? { + val context = ContextWrapper(InstrumentationRegistry.getInstrumentation().getContext()) + val inputManager = context.getSystemService(InputManager::class.java)!! + return inputManager.getKeyboardLayoutPreview(null, WIDTH, HEIGHT) + } + + @Test + fun testKeyboardLayoutDrawable_hasCorrectDimensions() { + setFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_LAYOUT_PREVIEW_FLAG) + val drawable = createDrawable()!! + assertEquals(WIDTH, drawable.intrinsicWidth) + assertEquals(HEIGHT, drawable.intrinsicHeight) + } + + @Test + fun testKeyboardLayoutDrawable_isNull_ifFlagOff() { + setFlagsRule.disableFlags(Flags.FLAG_KEYBOARD_LAYOUT_PREVIEW_FLAG) + assertNull(createDrawable()) + } +}
\ No newline at end of file diff --git a/tests/InputScreenshotTest/Android.bp b/tests/InputScreenshotTest/Android.bp new file mode 100644 index 000000000000..eee486f99748 --- /dev/null +++ b/tests/InputScreenshotTest/Android.bp @@ -0,0 +1,60 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "InputScreenshotTests", + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + platform_apis: true, + certificate: "platform", + static_libs: [ + "androidx.arch.core_core-testing", + "androidx.compose.ui_ui-test-junit4", + "androidx.compose.ui_ui-test-manifest", + "androidx.lifecycle_lifecycle-runtime-testing", + "androidx.compose.animation_animation", + "androidx.compose.material3_material3", + "androidx.compose.material_material-icons-extended", + "androidx.compose.runtime_runtime", + "androidx.compose.runtime_runtime-livedata", + "androidx.compose.ui_ui-tooling-preview", + "androidx.lifecycle_lifecycle-livedata-ktx", + "androidx.lifecycle_lifecycle-runtime-compose", + "androidx.navigation_navigation-compose", + "truth-prebuilt", + "androidx.compose.runtime_runtime", + "androidx.test.core", + "androidx.test.ext.junit", + "androidx.test.ext.truth", + "androidx.test.rules", + "androidx.test.runner", + "androidx.test.uiautomator_uiautomator", + "servicestests-utils", + "frameworks-base-testutils", + "platform-screenshot-diff-core", + "hamcrest-library", + "kotlin-test", + "flag-junit", + "platform-test-annotations", + "services.core.unboosted", + "testables", + "testng", + "truth-prebuilt", + ], + libs: [ + "android.test.mock", + "android.test.base", + ], + test_suites: ["device-tests"], + compile_multilib: "both", + use_embedded_native_libs: false, + asset_dirs: ["assets"], +} diff --git a/tests/InputScreenshotTest/AndroidManifest.xml b/tests/InputScreenshotTest/AndroidManifest.xml new file mode 100644 index 000000000000..9ffbb3a58ebb --- /dev/null +++ b/tests/InputScreenshotTest/AndroidManifest.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.input.screenshot"> + + <uses-sdk android:minSdkVersion="21"/> + + <application> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:label="Screenshot tests for Input" + android:targetPackage="com.android.input.screenshot"> + </instrumentation> +</manifest> diff --git a/tests/InputScreenshotTest/AndroidTest.xml b/tests/InputScreenshotTest/AndroidTest.xml new file mode 100644 index 000000000000..cc25fa454f06 --- /dev/null +++ b/tests/InputScreenshotTest/AndroidTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 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. + --> +<configuration description="Runs Input screendiff tests."> + <option name="test-suite-tag" value="apct-instrumentation" /> + <option name="test-suite-tag" value="apct" /> + <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> + <option name="optimized-property-setting" value="true" /> + </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="test-file-name" value="InputScreenshotTests.apk" /> + </target_preparer> + <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> + <option name="directory-keys" + value="/data/user/0/com.android.input.screenshot/files/input_screenshots" /> + <option name="collect-on-run-ended-only" value="true" /> + </metrics_collector> + <test class="com.android.tradefed.testtype.AndroidJUnitTest"> + <option name="package" value="com.android.input.screenshot" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + </test> +</configuration> diff --git a/tests/InputScreenshotTest/OWNERS b/tests/InputScreenshotTest/OWNERS new file mode 100644 index 000000000000..3cffce960b1c --- /dev/null +++ b/tests/InputScreenshotTest/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 136048 +include /core/java/android/hardware/input/OWNERS diff --git a/tests/InputScreenshotTest/TEST_MAPPING b/tests/InputScreenshotTest/TEST_MAPPING new file mode 100644 index 000000000000..727e609d91ac --- /dev/null +++ b/tests/InputScreenshotTest/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "postsubmit": [ + { + "name": "InputScreenshotTests" + } + ] +} diff --git a/tests/InputScreenshotTest/assets/phone/light_landscape_layout-preview.png b/tests/InputScreenshotTest/assets/phone/light_landscape_layout-preview.png Binary files differnew file mode 100644 index 000000000000..70e4a7101c7f --- /dev/null +++ b/tests/InputScreenshotTest/assets/phone/light_landscape_layout-preview.png diff --git a/tests/InputScreenshotTest/assets/phone/light_portrait_layout-preview-ansi.png b/tests/InputScreenshotTest/assets/phone/light_portrait_layout-preview-ansi.png Binary files differnew file mode 100644 index 000000000000..502c1b4499d4 --- /dev/null +++ b/tests/InputScreenshotTest/assets/phone/light_portrait_layout-preview-ansi.png diff --git a/tests/InputScreenshotTest/assets/phone/light_portrait_layout-preview-jis.png b/tests/InputScreenshotTest/assets/phone/light_portrait_layout-preview-jis.png Binary files differnew file mode 100644 index 000000000000..591b2fa9608e --- /dev/null +++ b/tests/InputScreenshotTest/assets/phone/light_portrait_layout-preview-jis.png diff --git a/tests/InputScreenshotTest/assets/phone/light_portrait_layout-preview.png b/tests/InputScreenshotTest/assets/phone/light_portrait_layout-preview.png Binary files differnew file mode 100644 index 000000000000..0137a853e538 --- /dev/null +++ b/tests/InputScreenshotTest/assets/phone/light_portrait_layout-preview.png diff --git a/tests/InputScreenshotTest/assets/tablet/dark_portrait_layout-preview.png b/tests/InputScreenshotTest/assets/tablet/dark_portrait_layout-preview.png Binary files differnew file mode 100644 index 000000000000..37a91e1fce53 --- /dev/null +++ b/tests/InputScreenshotTest/assets/tablet/dark_portrait_layout-preview.png diff --git a/tests/InputScreenshotTest/src/android/input/screenshot/Bitmap.kt b/tests/InputScreenshotTest/src/android/input/screenshot/Bitmap.kt new file mode 100644 index 000000000000..84c971c750fb --- /dev/null +++ b/tests/InputScreenshotTest/src/android/input/screenshot/Bitmap.kt @@ -0,0 +1,66 @@ +/* + * Copyright 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.input.screenshot + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.os.Build +import android.view.View +import platform.test.screenshot.matchers.MSSIMMatcher +import platform.test.screenshot.matchers.PixelPerfectMatcher + +/** Draw this [View] into a [Bitmap]. */ +// TODO(b/195673633): Remove this once Compose screenshot tests use hardware rendering for their +// tests. +fun View.drawIntoBitmap(): Bitmap { + val bitmap = + Bitmap.createBitmap( + measuredWidth, + measuredHeight, + Bitmap.Config.ARGB_8888, + ) + val canvas = Canvas(bitmap) + draw(canvas) + return bitmap +} + +/** + * The [BitmapMatcher][platform.test.screenshot.matchers.BitmapMatcher] that should be used for + * screenshot *unit* tests. + */ +val UnitTestBitmapMatcher = + if (Build.CPU_ABI == "x86_64") { + // Different CPU architectures can sometimes end up rendering differently, so we can't do + // pixel-perfect matching on different architectures using the same golden. Given that our + // presubmits are run on cf_x86_64_phone, our goldens should be perfectly matched on the + // x86_64 architecture and use the Structural Similarity Index on others. + // TODO(b/237511747): Run our screenshot presubmit tests on arm64 instead so that we can + // do pixel perfect matching both at presubmit time and at development time with actual + // devices. + PixelPerfectMatcher() + } else { + MSSIMMatcher() + } + +/** + * The [BitmapMatcher][platform.test.screenshot.matchers.BitmapMatcher] that should be used for + * screenshot *unit* tests. + * + * We use the Structural Similarity Index for integration tests because they usually contain + * additional information and noise that shouldn't break the test. + */ +val IntegrationTestBitmapMatcher = MSSIMMatcher()
\ No newline at end of file diff --git a/tests/InputScreenshotTest/src/android/input/screenshot/DefaultDeviceEmulationSpec.kt b/tests/InputScreenshotTest/src/android/input/screenshot/DefaultDeviceEmulationSpec.kt new file mode 100644 index 000000000000..edddc6b41cf7 --- /dev/null +++ b/tests/InputScreenshotTest/src/android/input/screenshot/DefaultDeviceEmulationSpec.kt @@ -0,0 +1,75 @@ +/* + * Copyright 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.input.screenshot + +import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.DisplaySpec + +/** + * The emulations specs for all 8 permutations of: + * - phone or tablet. + * - dark of light mode. + * - portrait or landscape. + */ +val DeviceEmulationSpec.Companion.PhoneAndTabletFull + get() = PhoneAndTabletFullSpec + +private val PhoneAndTabletFullSpec = + DeviceEmulationSpec.forDisplays(Displays.Phone, Displays.Tablet) + +/** + * The emulations specs of: + * - phone + light mode + portrait. + * - phone + light mode + landscape. + * - tablet + dark mode + portrait. + * + * This allows to test the most important permutations of a screen/layout with only 3 + * configurations. + */ +val DeviceEmulationSpec.Companion.PhoneAndTabletMinimal + get() = PhoneAndTabletMinimalSpec + +private val PhoneAndTabletMinimalSpec = + DeviceEmulationSpec.forDisplays(Displays.Phone, isDarkTheme = false) + + DeviceEmulationSpec.forDisplays(Displays.Tablet, isDarkTheme = true, isLandscape = false) + +/** + * This allows to test only single most important configuration. + */ +val DeviceEmulationSpec.Companion.PhoneMinimal + get() = PhoneMinimalSpec + +private val PhoneMinimalSpec = + DeviceEmulationSpec.forDisplays(Displays.Phone, isDarkTheme = false, isLandscape = false) + +object Displays { + val Phone = + DisplaySpec( + "phone", + width = 1440, + height = 3120, + densityDpi = 560, + ) + + val Tablet = + DisplaySpec( + "tablet", + width = 2560, + height = 1600, + densityDpi = 320, + ) +}
\ No newline at end of file diff --git a/tests/InputScreenshotTest/src/android/input/screenshot/InputGoldenImagePathManager.kt b/tests/InputScreenshotTest/src/android/input/screenshot/InputGoldenImagePathManager.kt new file mode 100644 index 000000000000..8faf22440828 --- /dev/null +++ b/tests/InputScreenshotTest/src/android/input/screenshot/InputGoldenImagePathManager.kt @@ -0,0 +1,44 @@ +/* + * Copyright 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.input.screenshot + +import androidx.test.platform.app.InstrumentationRegistry +import platform.test.screenshot.GoldenImagePathManager +import platform.test.screenshot.PathConfig + +/** A [GoldenImagePathManager] that should be used for all Input screenshot tests. */ +class InputGoldenImagePathManager( + pathConfig: PathConfig, + assetsPathRelativeToBuildRoot: String +) : + GoldenImagePathManager( + appContext = InstrumentationRegistry.getInstrumentation().context, + assetsPathRelativeToBuildRoot = assetsPathRelativeToBuildRoot, + deviceLocalPath = + InstrumentationRegistry.getInstrumentation() + .targetContext + .filesDir + .absolutePath + .toString() + "/input_screenshots", + pathConfig = pathConfig, + ) { + override fun toString(): String { + // This string is appended to all actual/expected screenshots on the device, so make sure + // it is a static value. + return "InputGoldenImagePathManager" + } +}
\ No newline at end of file diff --git a/tests/InputScreenshotTest/src/android/input/screenshot/InputScreenshotTestRule.kt b/tests/InputScreenshotTest/src/android/input/screenshot/InputScreenshotTestRule.kt new file mode 100644 index 000000000000..c2c3d5530a00 --- /dev/null +++ b/tests/InputScreenshotTest/src/android/input/screenshot/InputScreenshotTestRule.kt @@ -0,0 +1,87 @@ +/* + * Copyright 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.input.screenshot + +import android.content.Context +import android.graphics.Bitmap +import androidx.activity.ComponentActivity +import androidx.compose.foundation.Image +import androidx.compose.ui.platform.ViewRootForTest +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.graphics.asImageBitmap +import org.junit.rules.RuleChain +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import platform.test.screenshot.DeviceEmulationRule +import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.MaterialYouColorsRule +import platform.test.screenshot.ScreenshotTestRule +import platform.test.screenshot.getEmulatedDevicePathConfig + +/** A rule for Input screenshot diff tests. */ +class InputScreenshotTestRule( + emulationSpec: DeviceEmulationSpec, + assetsPathRelativeToBuildRoot: String +) : TestRule { + private val colorsRule = MaterialYouColorsRule() + private val deviceEmulationRule = DeviceEmulationRule(emulationSpec) + private val screenshotRule = + ScreenshotTestRule( + InputGoldenImagePathManager( + getEmulatedDevicePathConfig(emulationSpec), + assetsPathRelativeToBuildRoot + ) + ) + private val composeRule = createAndroidComposeRule<ComponentActivity>() + private val delegateRule = + RuleChain.outerRule(colorsRule) + .around(deviceEmulationRule) + .around(screenshotRule) + .around(composeRule) + private val matcher = UnitTestBitmapMatcher + + override fun apply(base: Statement, description: Description): Statement { + return delegateRule.apply(base, description) + } + + /** + * Compare [content] with the golden image identified by [goldenIdentifier]. + */ + fun screenshotTest( + goldenIdentifier: String, + content: (Context) -> Bitmap, + ) { + // Make sure that the activity draws full screen and fits the whole display. + val activity = composeRule.activity + activity.mainExecutor.execute { activity.window.setDecorFitsSystemWindows(false) } + + // Set the content using the AndroidComposeRule to make sure that the Activity is set up + // correctly. + composeRule.setContent { + Image( + bitmap = content(activity).asImageBitmap(), + contentDescription = null, + ) + } + composeRule.waitForIdle() + + val view = (composeRule.onRoot().fetchSemanticsNode().root as ViewRootForTest).view + screenshotRule.assertBitmapAgainstGolden(view.drawIntoBitmap(), goldenIdentifier, matcher) + } +}
\ No newline at end of file diff --git a/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewAnsiScreenshotTest.kt b/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewAnsiScreenshotTest.kt new file mode 100644 index 000000000000..e85578663764 --- /dev/null +++ b/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewAnsiScreenshotTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright 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.input.screenshot + +import android.content.Context +import android.hardware.input.KeyboardLayout +import android.os.LocaleList +import android.platform.test.flag.junit.SetFlagsRule +import com.android.hardware.input.Flags +import java.util.Locale +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import platform.test.screenshot.DeviceEmulationSpec + +/** A screenshot test for Keyboard layout preview for Ansi physical layout. */ +@RunWith(Parameterized::class) +class KeyboardLayoutPreviewAnsiScreenshotTest(emulationSpec: DeviceEmulationSpec) { + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getTestSpecs() = DeviceEmulationSpec.PhoneMinimal + } + + val setFlagsRule = SetFlagsRule() + val screenshotRule = InputScreenshotTestRule( + emulationSpec, + "frameworks/base/tests/InputScreenshotTest/assets" + ) + + @get:Rule + val ruleChain = RuleChain.outerRule(screenshotRule).around(setFlagsRule) + + @Test + fun test() { + setFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_LAYOUT_PREVIEW_FLAG) + screenshotRule.screenshotTest("layout-preview-ansi") { + context: Context -> LayoutPreview.createLayoutPreview( + context, + KeyboardLayout( + "descriptor", + "layout", + /* collection= */null, + /* priority= */0, + LocaleList(Locale.US), + /* layoutType= */0, + /* vid= */0, + /* pid= */0 + ) + ) + } + } + +}
\ No newline at end of file diff --git a/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewIsoScreenshotTest.kt b/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewIsoScreenshotTest.kt new file mode 100644 index 000000000000..8ae6dfd8b63b --- /dev/null +++ b/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewIsoScreenshotTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright 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.input.screenshot + +import android.content.Context +import android.hardware.input.KeyboardLayout +import android.os.LocaleList +import android.platform.test.flag.junit.SetFlagsRule +import com.android.hardware.input.Flags +import java.util.Locale +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import platform.test.screenshot.DeviceEmulationSpec + +/** A screenshot test for Keyboard layout preview for Iso physical layout. */ +@RunWith(Parameterized::class) +class KeyboardLayoutPreviewIsoScreenshotTest(emulationSpec: DeviceEmulationSpec) { + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getTestSpecs() = DeviceEmulationSpec.PhoneAndTabletMinimal + } + + val setFlagsRule = SetFlagsRule() + val screenshotRule = InputScreenshotTestRule( + emulationSpec, + "frameworks/base/tests/InputScreenshotTest/assets" + ) + + @get:Rule + val ruleChain = RuleChain.outerRule(screenshotRule).around(setFlagsRule) + + @Test + fun test() { + setFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_LAYOUT_PREVIEW_FLAG) + screenshotRule.screenshotTest("layout-preview") { + context: Context -> LayoutPreview.createLayoutPreview(context, null) + } + } + +}
\ No newline at end of file diff --git a/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewJisScreenshotTest.kt b/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewJisScreenshotTest.kt new file mode 100644 index 000000000000..5231c14bfc9a --- /dev/null +++ b/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewJisScreenshotTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright 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.input.screenshot + +import android.content.Context +import android.hardware.input.KeyboardLayout +import android.os.LocaleList +import android.platform.test.flag.junit.SetFlagsRule +import com.android.hardware.input.Flags +import java.util.Locale +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import platform.test.screenshot.DeviceEmulationSpec + +/** A screenshot test for Keyboard layout preview for JIS physical layout. */ +@RunWith(Parameterized::class) +class KeyboardLayoutPreviewJisScreenshotTest(emulationSpec: DeviceEmulationSpec) { + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getTestSpecs() = DeviceEmulationSpec.PhoneMinimal + } + + val setFlagsRule = SetFlagsRule() + val screenshotRule = InputScreenshotTestRule( + emulationSpec, + "frameworks/base/tests/InputScreenshotTest/assets" + ) + + @get:Rule + val ruleChain = RuleChain.outerRule(screenshotRule).around(setFlagsRule) + + @Test + fun test() { + setFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_LAYOUT_PREVIEW_FLAG) + screenshotRule.screenshotTest("layout-preview-jis") { + context: Context -> LayoutPreview.createLayoutPreview( + context, + KeyboardLayout( + "descriptor", + "layout", + /* collection= */null, + /* priority= */0, + LocaleList(Locale.JAPAN), + /* layoutType= */0, + /* vid= */0, + /* pid= */0 + ) + ) + } + } + +}
\ No newline at end of file diff --git a/tests/InputScreenshotTest/src/android/input/screenshot/LayoutPreview.kt b/tests/InputScreenshotTest/src/android/input/screenshot/LayoutPreview.kt new file mode 100644 index 000000000000..76ee3791011b --- /dev/null +++ b/tests/InputScreenshotTest/src/android/input/screenshot/LayoutPreview.kt @@ -0,0 +1,39 @@ +/* + * Copyright 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. + */ + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.hardware.input.InputManager +import android.hardware.input.KeyboardLayout +import android.util.TypedValue +import kotlin.math.roundToInt + +object LayoutPreview { + fun createLayoutPreview(context: Context, layout: KeyboardLayout?): Bitmap { + val im = context.getSystemService(InputManager::class.java)!! + val width = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + 600.0F, context.getResources().getDisplayMetrics()).roundToInt() + val height = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + 200.0F, context.getResources().getDisplayMetrics()).roundToInt() + val drawable = im.getKeyboardLayoutPreview(layout, width, height)!! + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()) + drawable.draw(canvas) + return bitmap + } +}
\ No newline at end of file |