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