Add support for Keyboard layout preview
Screenshot for drawable: https://screenshot.googleplex.com/9hBauxZB9asdmNN
Flag: com.android.hardware.input.keyboardLayoutPreviewFlag
Test: atest KeyboardLayoutPreviewTests
Test: atest KeyboardLayoutPreviewScreenshotTests
Bug: 293579375
Change-Id: If66c2108c3a65986278d8d332533fb9d1ca6b266
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl
index c3fae55..88d7231 100644
--- a/core/java/android/hardware/input/IInputManager.aidl
+++ b/core/java/android/hardware/input/IInputManager.aidl
@@ -41,6 +41,7 @@
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 @@
// 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 a0cceae..ff1a6ac 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.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 @@
}
/**
+ * 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 e886f68..8c598ae 100644
--- a/core/java/android/hardware/input/InputManagerGlobal.java
+++ b/core/java/android/hardware/input/InputManagerGlobal.java
@@ -51,6 +51,7 @@
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 @@
}
/**
+ * 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 4403251..bbfed24 100644
--- a/core/java/android/hardware/input/KeyboardLayout.java
+++ b/core/java/android/hardware/input/KeyboardLayout.java
@@ -22,6 +22,7 @@
import android.os.Parcelable;
import java.util.HashMap;
+import java.util.Locale;
import java.util.Map;
import java.util.Objects;
@@ -230,6 +231,33 @@
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 0000000..d943c37
--- /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 0000000..241c452
--- /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 0000000..1a761a67
--- /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 d8221a6..4fe53c2 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 @@
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 @@
}
/**
+ * 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 ddaeb5a..7f69e22 100644
--- a/core/jni/android_view_KeyCharacterMap.cpp
+++ b/core/jni/android_view_KeyCharacterMap.cpp
@@ -240,6 +240,36 @@
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 @@
{"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 62660c4..6b399de 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.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 @@
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 a5162c0..0eb620f 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.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 @@
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 96b685d..365e00e 100644
--- a/tests/Input/Android.bp
+++ b/tests/Input/Android.bp
@@ -26,6 +26,7 @@
"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 0000000..3a2a3be
--- /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 0000000..eee486f
--- /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 0000000..9ffbb3a
--- /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 0000000..cc25fa4
--- /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 0000000..3cffce9
--- /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 0000000..727e609
--- /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
new file mode 100644
index 0000000..70e4a71
--- /dev/null
+++ b/tests/InputScreenshotTest/assets/phone/light_landscape_layout-preview.png
Binary files differ
diff --git a/tests/InputScreenshotTest/assets/phone/light_portrait_layout-preview-ansi.png b/tests/InputScreenshotTest/assets/phone/light_portrait_layout-preview-ansi.png
new file mode 100644
index 0000000..502c1b4
--- /dev/null
+++ b/tests/InputScreenshotTest/assets/phone/light_portrait_layout-preview-ansi.png
Binary files differ
diff --git a/tests/InputScreenshotTest/assets/phone/light_portrait_layout-preview-jis.png b/tests/InputScreenshotTest/assets/phone/light_portrait_layout-preview-jis.png
new file mode 100644
index 0000000..591b2fa
--- /dev/null
+++ b/tests/InputScreenshotTest/assets/phone/light_portrait_layout-preview-jis.png
Binary files differ
diff --git a/tests/InputScreenshotTest/assets/phone/light_portrait_layout-preview.png b/tests/InputScreenshotTest/assets/phone/light_portrait_layout-preview.png
new file mode 100644
index 0000000..0137a85
--- /dev/null
+++ b/tests/InputScreenshotTest/assets/phone/light_portrait_layout-preview.png
Binary files differ
diff --git a/tests/InputScreenshotTest/assets/tablet/dark_portrait_layout-preview.png b/tests/InputScreenshotTest/assets/tablet/dark_portrait_layout-preview.png
new file mode 100644
index 0000000..37a91e1
--- /dev/null
+++ b/tests/InputScreenshotTest/assets/tablet/dark_portrait_layout-preview.png
Binary files differ
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 0000000..84c971c
--- /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 0000000..edddc6b4
--- /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 0000000..8faf224
--- /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 0000000..c2c3d55
--- /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 0000000..e855786
--- /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 0000000..8ae6dfd
--- /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 0000000..5231c14
--- /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 0000000..76ee379
--- /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