Merge "Switch and store keyboard layouts based on IME subtype."
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl
index 5d969b1..c4ee347 100644
--- a/core/java/android/hardware/input/IInputManager.aidl
+++ b/core/java/android/hardware/input/IInputManager.aidl
@@ -25,6 +25,8 @@
 import android.view.InputDevice;
 import android.view.InputEvent;
 import android.view.PointerIcon;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodSubtype;
 
 /** @hide */
 interface IInputManager {
@@ -59,6 +61,11 @@
             String keyboardLayoutDescriptor);
     void removeKeyboardLayoutForInputDevice(in InputDeviceIdentifier identifier,
             String keyboardLayoutDescriptor);
+    KeyboardLayout getKeyboardLayoutForInputDevice(in InputDeviceIdentifier identifier,
+            in InputMethodInfo imeInfo, in InputMethodSubtype imeSubtype);
+    void setKeyboardLayoutForInputDevice(in InputDeviceIdentifier identifier,
+            in InputMethodInfo imeInfo, in InputMethodSubtype imeSubtype,
+            String keyboardLayoutDescriptor);
 
     // Registers an input devices changed listener.
     void registerInputDevicesChangedListener(IInputDevicesChangedListener listener);
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index 9972f49..cbe3412 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -16,7 +16,7 @@
 
 package android.hardware.input;
 
-import android.view.PointerIcon;
+import com.android.internal.inputmethod.InputMethodSubtypeHandle;
 import com.android.internal.os.SomeArgs;
 import com.android.internal.util.ArrayUtils;
 
@@ -40,6 +40,9 @@
 import android.util.SparseArray;
 import android.view.InputDevice;
 import android.view.InputEvent;
+import android.view.PointerIcon;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodSubtype;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -643,6 +646,51 @@
         }
     }
 
+
+    /**
+     * Gets the keyboard layout for the specified input device and IME subtype.
+     *
+     * @param identifier The identifier for the input device.
+     * @param inputMethodInfo The input method.
+     * @param inputMethodSubtype The input method subtype.
+     *
+     * @return The associated {@link KeyboardLayout}, or null if one has not been set.
+     *
+     * @hide
+     */
+    public KeyboardLayout getKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
+            InputMethodInfo inputMethodInfo, InputMethodSubtype inputMethodSubtype) {
+        try {
+            return mIm.getKeyboardLayoutForInputDevice(
+                    identifier, inputMethodInfo, inputMethodSubtype);
+        } catch (RemoteException ex) {
+            Log.w(TAG, "Could not set keyboard layout.", ex);
+            return null;
+        }
+    }
+
+    /**
+     * Sets the keyboard layout for the specified input device and IME subtype pair.
+     *
+     * @param identifier The identifier for the input device.
+     * @param inputMethodInfo The input method with which to associate the keyboard layout.
+     * @param inputMethodSubtype The input method subtype which which to associate the keyboard
+     *                           layout.
+     * @param keyboardLayoutDescriptor The descriptor of the keyboard layout to set
+     *
+     * @hide
+     */
+    public void setKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
+            InputMethodInfo inputMethodInfo, InputMethodSubtype inputMethodSubtype,
+            String keyboardLayoutDescriptor) {
+        try {
+            mIm.setKeyboardLayoutForInputDevice(identifier, inputMethodInfo,
+                    inputMethodSubtype, keyboardLayoutDescriptor);
+        } catch (RemoteException ex) {
+            Log.w(TAG, "Could not set keyboard layout.", ex);
+        }
+    }
+
     /**
      * Gets the TouchCalibration applied to the specified input device's coordinates.
      *
diff --git a/core/java/android/hardware/input/TouchCalibration.java b/core/java/android/hardware/input/TouchCalibration.java
index 025fad0..15503ed 100644
--- a/core/java/android/hardware/input/TouchCalibration.java
+++ b/core/java/android/hardware/input/TouchCalibration.java
@@ -123,4 +123,10 @@
                Float.floatToIntBits(mYScale)  ^
                Float.floatToIntBits(mYOffset);
     }
+
+    @Override
+    public String toString() {
+        return String.format("[%f, %f, %f, %f, %f, %f]",
+                mXScale, mXYMix, mXOffset, mYXMix, mYScale, mYOffset);
+    }
 }
diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java
index 4a1142f..6e38b32 100644
--- a/core/java/android/view/WindowManagerPolicy.java
+++ b/core/java/android/view/WindowManagerPolicy.java
@@ -441,12 +441,6 @@
         public int getCameraLensCoverState();
 
         /**
-         * Switch the keyboard layout for the given device.
-         * Direction should be +1 or -1 to go to the next or previous keyboard layout.
-         */
-        public void switchKeyboardLayout(int deviceId, int direction);
-
-        /**
          * Switch the input method, to be precise, input method subtype.
          *
          * @param forwardDirection {@code true} to rotate in a forward direction.
diff --git a/core/java/com/android/internal/inputmethod/InputMethodSubtypeHandle.java b/core/java/com/android/internal/inputmethod/InputMethodSubtypeHandle.java
new file mode 100644
index 0000000..975021e8
--- /dev/null
+++ b/core/java/com/android/internal/inputmethod/InputMethodSubtypeHandle.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2016 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.internal.inputmethod;
+
+import android.text.TextUtils;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodSubtype;
+
+import java.util.Objects;
+
+public class InputMethodSubtypeHandle {
+    private final String mInputMethodId;
+    private final int mSubtypeId;
+
+    public InputMethodSubtypeHandle(InputMethodInfo info, InputMethodSubtype subtype) {
+        mInputMethodId = info.getId();
+        if (subtype != null) {
+            mSubtypeId = subtype.hashCode();
+        } else {
+            mSubtypeId = 0;
+        }
+    }
+
+    public InputMethodSubtypeHandle(String inputMethodId, int subtypeId) {
+        mInputMethodId = inputMethodId;
+        mSubtypeId = subtypeId;
+    }
+
+    public String getInputMethodId() {
+        return mInputMethodId;
+    }
+
+    public int getSubtypeId() {
+        return mSubtypeId;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == null || !(o instanceof InputMethodSubtypeHandle)) {
+            return false;
+        }
+        InputMethodSubtypeHandle other = (InputMethodSubtypeHandle) o;
+        return TextUtils.equals(mInputMethodId, other.getInputMethodId())
+                && mSubtypeId == other.getSubtypeId();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(mInputMethodId) * 31 + mSubtypeId;
+    }
+
+    @Override
+    public String toString() {
+        return "InputMethodSubtypeHandle{mInputMethodId=" + mInputMethodId
+            + ", mSubtypeId=" + mSubtypeId + "}";
+    }
+}
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index ee91b63..573afd6 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -18,6 +18,7 @@
 
 import android.annotation.Nullable;
 import android.view.Display;
+import com.android.internal.inputmethod.InputMethodSubtypeHandle;
 import com.android.internal.os.SomeArgs;
 import com.android.internal.R;
 import com.android.internal.util.XmlUtils;
@@ -67,6 +68,8 @@
 import android.os.MessageQueue;
 import android.os.Process;
 import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ShellCommand;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.provider.Settings.SettingNotFoundException;
@@ -97,6 +100,7 @@
 import java.io.InputStreamReader;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -157,8 +161,7 @@
     private final ArrayList<InputDevice>
             mTempFullKeyboards = new ArrayList<InputDevice>(); // handler thread only
     private boolean mKeyboardLayoutNotificationShown;
-    private PendingIntent mKeyboardLayoutIntent;
-    private Toast mSwitchedKeyboardLayoutToast;
+    private InputMethodSubtypeHandle mCurrentImeHandle;
 
     // State for vibrator tokens.
     private Object mVibratorLock = new Object();
@@ -1297,6 +1300,82 @@
     }
 
     @Override // Binder call
+    @Nullable
+    public KeyboardLayout getKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
+            InputMethodInfo imeInfo, InputMethodSubtype imeSubtype) {
+        InputMethodSubtypeHandle handle = new InputMethodSubtypeHandle(imeInfo, imeSubtype);
+        String key = getLayoutDescriptor(identifier);
+        final String keyboardLayoutDescriptor;
+        synchronized (mDataStore) {
+            keyboardLayoutDescriptor = mDataStore.getKeyboardLayout(key, handle);
+        }
+
+        if (keyboardLayoutDescriptor == null) {
+            return null;
+        }
+
+        final KeyboardLayout[] result = new KeyboardLayout[1];
+        visitKeyboardLayout(keyboardLayoutDescriptor, new KeyboardLayoutVisitor() {
+            @Override
+            public void visitKeyboardLayout(Resources resources,
+                    int keyboardLayoutResId, KeyboardLayout layout) {
+                result[0] = layout;
+            }
+        });
+        if (result[0] == null) {
+            Slog.w(TAG, "Could not get keyboard layout with descriptor '"
+                    + keyboardLayoutDescriptor + "'.");
+        }
+        return result[0];
+    }
+
+    @Override
+    public void setKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
+            InputMethodInfo imeInfo, InputMethodSubtype imeSubtype,
+            String keyboardLayoutDescriptor) {
+        if (!checkCallingPermission(android.Manifest.permission.SET_KEYBOARD_LAYOUT,
+                "setKeyboardLayoutForInputDevice()")) {
+            throw new SecurityException("Requires SET_KEYBOARD_LAYOUT permission");
+        }
+        if (keyboardLayoutDescriptor == null) {
+            throw new IllegalArgumentException("keyboardLayoutDescriptor must not be null");
+        }
+        if (imeInfo == null || imeSubtype == null) {
+            throw new IllegalArgumentException("imeInfo and imeSubtype must not be null");
+        }
+        InputMethodSubtypeHandle handle = new InputMethodSubtypeHandle(imeInfo, imeSubtype);
+        setKeyboardLayoutForInputDeviceInner(identifier, handle, keyboardLayoutDescriptor);
+    }
+
+    private void setKeyboardLayoutForInputDeviceInner(InputDeviceIdentifier identifier,
+            InputMethodSubtypeHandle imeHandle, String keyboardLayoutDescriptor) {
+        String key = getLayoutDescriptor(identifier);
+        synchronized (mDataStore) {
+            try {
+                if (mDataStore.setKeyboardLayout(key, imeHandle, keyboardLayoutDescriptor)) {
+                    if (DEBUG) {
+                        Slog.d(TAG, "Set keyboard layout " + keyboardLayoutDescriptor +
+                                " for subtype " + imeHandle + " and device " + identifier +
+                                " using key " + key);
+                    }
+                    if (imeHandle.equals(mCurrentImeHandle)) {
+                        if (DEBUG) {
+                            Slog.d(TAG, "Layout for current subtype changed, switching layout");
+                        }
+                        SomeArgs args = SomeArgs.obtain();
+                        args.arg1 = identifier;
+                        args.arg2 = imeHandle;
+                        mHandler.obtainMessage(MSG_SWITCH_KEYBOARD_LAYOUT, args).sendToTarget();
+                    }
+                    mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
+                }
+            } finally {
+                mDataStore.saveIfNeeded();
+            }
+        }
+    }
+
+    @Override // Binder call
     public void addKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
             String keyboardLayoutDescriptor) {
         if (!checkCallingPermission(android.Manifest.permission.SET_KEYBOARD_LAYOUT,
@@ -1315,8 +1394,7 @@
                     oldLayout = mDataStore.getCurrentKeyboardLayout(identifier.getDescriptor());
                 }
                 if (mDataStore.addKeyboardLayout(key, keyboardLayoutDescriptor)
-                        && !Objects.equal(oldLayout,
-                                mDataStore.getCurrentKeyboardLayout(key))) {
+                        && !Objects.equal(oldLayout, mDataStore.getCurrentKeyboardLayout(key))) {
                     mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
                 }
             } finally {
@@ -1366,45 +1444,44 @@
             Slog.i(TAG, "InputMethodSubtype changed: userId=" + userId
                     + " ime=" + inputMethodInfo + " subtype=" + subtype);
         }
-    }
-
-    public void switchKeyboardLayout(int deviceId, int direction) {
-        mHandler.obtainMessage(MSG_SWITCH_KEYBOARD_LAYOUT, deviceId, direction).sendToTarget();
+        if (inputMethodInfo == null) {
+            Slog.d(TAG, "No InputMethod is running, ignoring change");
+            return;
+        }
+        if (subtype != null && !"keyboard".equals(subtype.getMode())) {
+            Slog.d(TAG, "InputMethodSubtype changed to non-keyboard subtype, ignoring change");
+            return;
+        }
+        InputMethodSubtypeHandle handle = new InputMethodSubtypeHandle(inputMethodInfo, subtype);
+        if (!handle.equals(mCurrentImeHandle)) {
+            mCurrentImeHandle = handle;
+            handleSwitchKeyboardLayout(null, handle);
+        }
     }
 
     // Must be called on handler.
-    private void handleSwitchKeyboardLayout(int deviceId, int direction) {
-        final InputDevice device = getInputDevice(deviceId);
-        if (device != null) {
-            final boolean changed;
-            final String keyboardLayoutDescriptor;
-
-            String key = getLayoutDescriptor(device.getIdentifier());
-            synchronized (mDataStore) {
-                try {
-                    changed = mDataStore.switchKeyboardLayout(key, direction);
-                    keyboardLayoutDescriptor = mDataStore.getCurrentKeyboardLayout(
-                            key);
-                } finally {
-                    mDataStore.saveIfNeeded();
+    private void handleSwitchKeyboardLayout(@Nullable InputDeviceIdentifier identifier,
+            InputMethodSubtypeHandle handle) {
+        synchronized (mInputDevicesLock) {
+            for (InputDevice device : mInputDevices) {
+                if (identifier != null && !device.getIdentifier().equals(identifier) ||
+                        !device.isFullKeyboard()) {
+                    continue;
                 }
-            }
-
-            if (changed) {
-                if (mSwitchedKeyboardLayoutToast != null) {
-                    mSwitchedKeyboardLayoutToast.cancel();
-                    mSwitchedKeyboardLayoutToast = null;
-                }
-                if (keyboardLayoutDescriptor != null) {
-                    KeyboardLayout keyboardLayout = getKeyboardLayout(keyboardLayoutDescriptor);
-                    if (keyboardLayout != null) {
-                        mSwitchedKeyboardLayoutToast = Toast.makeText(
-                                mContext, keyboardLayout.getLabel(), Toast.LENGTH_SHORT);
-                        mSwitchedKeyboardLayoutToast.show();
+                String key = getLayoutDescriptor(device.getIdentifier());
+                boolean changed = false;
+                synchronized (mDataStore) {
+                    try {
+                        if (mDataStore.switchKeyboardLayout(key, handle)) {
+                            changed = true;
+                        }
+                    } finally {
+                        mDataStore.saveIfNeeded();
                     }
                 }
-
-                reloadKeyboardLayouts();
+                if (changed) {
+                    reloadKeyboardLayouts();
+                }
             }
         }
     }
@@ -1616,7 +1693,7 @@
     }
 
     @Override
-    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+    public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
         if (mContext.checkCallingOrSelfPermission(Manifest.permission.DUMP)
                 != PackageManager.PERMISSION_GRANTED) {
             pw.println("Permission Denial: can't dump InputManager from from pid="
@@ -1630,8 +1707,48 @@
         if (dumpStr != null) {
             pw.println(dumpStr);
         }
+        pw.println("  Keyboard Layouts:");
+        visitAllKeyboardLayouts(new KeyboardLayoutVisitor() {
+            @Override
+            public void visitKeyboardLayout(Resources resources,
+                    int keyboardLayoutResId, KeyboardLayout layout) {
+                pw.println("    \"" + layout + "\": " + layout.getDescriptor());
+            }
+        });
+        pw.println();
+        synchronized(mDataStore) {
+            mDataStore.dump(pw, "  ");
+        }
     }
 
+    @Override
+    public void onShellCommand(FileDescriptor in, FileDescriptor out,
+            FileDescriptor err, String[] args, ResultReceiver resultReceiver) {
+        (new Shell()).exec(this, in, out, err, args, resultReceiver);
+    }
+
+    public int onShellCommand(Shell shell, String cmd) {
+        if (TextUtils.isEmpty(cmd)) {
+            shell.onHelp();
+            return 1;
+        }
+        if (cmd.equals("setlayout")) {
+            if (!checkCallingPermission(android.Manifest.permission.SET_KEYBOARD_LAYOUT,
+                    "onShellCommand()")) {
+                throw new SecurityException("Requires SET_KEYBOARD_LAYOUT permission");
+            }
+            InputMethodSubtypeHandle handle = new InputMethodSubtypeHandle(
+                    shell.getNextArgRequired(), Integer.parseInt(shell.getNextArgRequired()));
+            String descriptor = shell.getNextArgRequired();
+            int vid = Integer.decode(shell.getNextArgRequired());
+            int pid = Integer.decode(shell.getNextArgRequired());
+            InputDeviceIdentifier id = new InputDeviceIdentifier(descriptor, vid, pid);
+            setKeyboardLayoutForInputDeviceInner(id, handle, shell.getNextArgRequired());
+        }
+        return 0;
+    }
+
+
     private boolean checkCallingPermission(String permission, String func) {
         // Quick check: if the calling permission is me, it's all okay.
         if (Binder.getCallingPid() == Process.myPid()) {
@@ -1937,9 +2054,12 @@
                 case MSG_DELIVER_INPUT_DEVICES_CHANGED:
                     deliverInputDevicesChanged((InputDevice[])msg.obj);
                     break;
-                case MSG_SWITCH_KEYBOARD_LAYOUT:
-                    handleSwitchKeyboardLayout(msg.arg1, msg.arg2);
+                case MSG_SWITCH_KEYBOARD_LAYOUT: {
+                    SomeArgs args = (SomeArgs)msg.obj;
+                    handleSwitchKeyboardLayout((InputDeviceIdentifier)args.arg1,
+                            (InputMethodSubtypeHandle)args.arg2);
                     break;
+                }
                 case MSG_RELOAD_KEYBOARD_LAYOUTS:
                     reloadKeyboardLayouts();
                     break;
@@ -2106,6 +2226,25 @@
         }
     }
 
+    private class Shell extends ShellCommand {
+        @Override
+        public int onCommand(String cmd) {
+            return onShellCommand(this, cmd);
+        }
+
+        @Override
+        public void onHelp() {
+            final PrintWriter pw = getOutPrintWriter();
+            pw.println("Input manager commands:");
+            pw.println("  help");
+            pw.println("    Print this help text.");
+            pw.println("");
+            pw.println("  setlayout IME_ID IME_SUPTYPE_HASH_CODE"
+                    + " DEVICE_DESCRIPTOR VENDOR_ID PRODUCT_ID KEYBOARD_DESCRIPTOR");
+            pw.println("    Sets a keyboard layout for a given IME subtype and input device pair");
+        }
+    }
+
     private final class LocalService extends InputManagerInternal {
         @Override
         public void setDisplayViewports(
diff --git a/services/core/java/com/android/server/input/PersistentDataStore.java b/services/core/java/com/android/server/input/PersistentDataStore.java
index f6d7244..e97aca8 100644
--- a/services/core/java/com/android/server/input/PersistentDataStore.java
+++ b/services/core/java/com/android/server/input/PersistentDataStore.java
@@ -16,6 +16,7 @@
 
 package com.android.server.input;
 
+import com.android.internal.inputmethod.InputMethodSubtypeHandle;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.FastXmlSerializer;
 import com.android.internal.util.XmlUtils;
@@ -26,6 +27,8 @@
 
 import android.view.Surface;
 import android.hardware.input.TouchCalibration;
+import android.text.TextUtils;
+import android.util.ArrayMap;
 import android.util.AtomicFile;
 import android.util.Slog;
 import android.util.Xml;
@@ -37,10 +40,13 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.PrintWriter;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -131,9 +137,26 @@
         }
         return state.getKeyboardLayouts();
     }
+    public String getKeyboardLayout(String inputDeviceDescriptor,
+            InputMethodSubtypeHandle imeHandle) {
+        InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, false);
+        if (state == null) {
+            return null;
+        }
+        return state.getKeyboardLayout(imeHandle);
+    }
 
-    public boolean addKeyboardLayout(String inputDeviceDescriptor,
-            String keyboardLayoutDescriptor) {
+    public boolean setKeyboardLayout(String inputDeviceDescriptor,
+            InputMethodSubtypeHandle imeHandle, String keyboardLayoutDescriptor) {
+        InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, true);
+        if (state.setKeyboardLayout(imeHandle, keyboardLayoutDescriptor)) {
+            setDirty();
+            return true;
+        }
+        return false;
+    }
+
+    public boolean addKeyboardLayout(String inputDeviceDescriptor, String keyboardLayoutDescriptor) {
         InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, true);
         if (state.addKeyboardLayout(keyboardLayoutDescriptor)) {
             setDirty();
@@ -152,9 +175,10 @@
         return false;
     }
 
-    public boolean switchKeyboardLayout(String inputDeviceDescriptor, int direction) {
+    public boolean switchKeyboardLayout(String inputDeviceDescriptor,
+            InputMethodSubtypeHandle imeHandle) {
         InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, false);
-        if (state != null && state.switchKeyboardLayout(direction)) {
+        if (state != null && state.switchKeyboardLayout(imeHandle)) {
             setDirty();
             return true;
         }
@@ -301,13 +325,26 @@
         serializer.endDocument();
     }
 
+    public void dump(PrintWriter pw, String prefix) {
+        pw.println(prefix + "PersistentDataStore");
+        pw.println(prefix + "  mLoaded=" + mLoaded);
+        pw.println(prefix + "  mDirty=" + mDirty);
+        pw.println(prefix + "  InputDeviceStates:");
+        int i = 0;
+        for (Map.Entry<String, InputDeviceState> entry : mInputDevices.entrySet()) {
+            pw.println(prefix + "    " + i++ + ": " + entry.getKey());
+            entry.getValue().dump(pw, prefix + "      ");
+        }
+    }
+
     private static final class InputDeviceState {
         private static final String[] CALIBRATION_NAME = { "x_scale",
                 "x_ymix", "x_offset", "y_xmix", "y_scale", "y_offset" };
 
         private TouchCalibration[] mTouchCalibration = new TouchCalibration[4];
         private String mCurrentKeyboardLayout;
-        private ArrayList<String> mKeyboardLayouts = new ArrayList<String>();
+        private List<String> mUnassociatedKeyboardLayouts = new ArrayList<>();
+        private ArrayMap<InputMethodSubtypeHandle, String> mKeyboardLayouts = new ArrayMap<>();
 
         public TouchCalibration getTouchCalibration(int surfaceRotation) {
             try {
@@ -345,18 +382,34 @@
         }
 
         public String[] getKeyboardLayouts() {
-            if (mKeyboardLayouts.isEmpty()) {
+            if (mUnassociatedKeyboardLayouts.isEmpty()) {
                 return (String[])ArrayUtils.emptyArray(String.class);
             }
-            return mKeyboardLayouts.toArray(new String[mKeyboardLayouts.size()]);
+            return mUnassociatedKeyboardLayouts.toArray(
+                    new String[mUnassociatedKeyboardLayouts.size()]);
+        }
+
+        public String getKeyboardLayout(InputMethodSubtypeHandle handle) {
+            return mKeyboardLayouts.get(handle);
+        }
+
+        public boolean setKeyboardLayout(InputMethodSubtypeHandle imeHandle,
+                String keyboardLayout) {
+            String existingLayout = mKeyboardLayouts.get(imeHandle);
+            if (TextUtils.equals(existingLayout, keyboardLayout)) {
+                return false;
+            }
+            mKeyboardLayouts.put(imeHandle, keyboardLayout);
+            return true;
         }
 
         public boolean addKeyboardLayout(String keyboardLayout) {
-            int index = Collections.binarySearch(mKeyboardLayouts, keyboardLayout);
+            int index = Collections.binarySearch(
+                    mUnassociatedKeyboardLayouts, keyboardLayout);
             if (index >= 0) {
                 return false;
             }
-            mKeyboardLayouts.add(-index - 1, keyboardLayout);
+            mUnassociatedKeyboardLayouts.add(-index - 1, keyboardLayout);
             if (mCurrentKeyboardLayout == null) {
                 mCurrentKeyboardLayout = keyboardLayout;
             }
@@ -364,11 +417,11 @@
         }
 
         public boolean removeKeyboardLayout(String keyboardLayout) {
-            int index = Collections.binarySearch(mKeyboardLayouts, keyboardLayout);
+            int index = Collections.binarySearch(mUnassociatedKeyboardLayouts, keyboardLayout);
             if (index < 0) {
                 return false;
             }
-            mKeyboardLayouts.remove(index);
+            mUnassociatedKeyboardLayouts.remove(index);
             updateCurrentKeyboardLayoutIfRemoved(keyboardLayout, index);
             return true;
         }
@@ -376,41 +429,34 @@
         private void updateCurrentKeyboardLayoutIfRemoved(
                 String removedKeyboardLayout, int removedIndex) {
             if (Objects.equal(mCurrentKeyboardLayout, removedKeyboardLayout)) {
-                if (!mKeyboardLayouts.isEmpty()) {
+                if (!mUnassociatedKeyboardLayouts.isEmpty()) {
                     int index = removedIndex;
-                    if (index == mKeyboardLayouts.size()) {
+                    if (index == mUnassociatedKeyboardLayouts.size()) {
                         index = 0;
                     }
-                    mCurrentKeyboardLayout = mKeyboardLayouts.get(index);
+                    mCurrentKeyboardLayout = mUnassociatedKeyboardLayouts.get(index);
                 } else {
                     mCurrentKeyboardLayout = null;
                 }
             }
         }
 
-        public boolean switchKeyboardLayout(int direction) {
-            final int size = mKeyboardLayouts.size();
-            if (size < 2) {
-                return false;
+        public boolean switchKeyboardLayout(InputMethodSubtypeHandle imeHandle) {
+            final String layout = mKeyboardLayouts.get(imeHandle);
+            if (layout != null && !TextUtils.equals(mCurrentKeyboardLayout, layout)) {
+                mCurrentKeyboardLayout = layout;
+                return true;
             }
-            int index = Collections.binarySearch(mKeyboardLayouts, mCurrentKeyboardLayout);
-            assert index >= 0;
-            if (direction > 0) {
-                index = (index + 1) % size;
-            } else {
-                index = (index + size - 1) % size;
-            }
-            mCurrentKeyboardLayout = mKeyboardLayouts.get(index);
-            return true;
+            return false;
         }
 
         public boolean removeUninstalledKeyboardLayouts(Set<String> availableKeyboardLayouts) {
             boolean changed = false;
-            for (int i = mKeyboardLayouts.size(); i-- > 0; ) {
-                String keyboardLayout = mKeyboardLayouts.get(i);
+            for (int i = mUnassociatedKeyboardLayouts.size(); i-- > 0; ) {
+                String keyboardLayout = mUnassociatedKeyboardLayouts.get(i);
                 if (!availableKeyboardLayouts.contains(keyboardLayout)) {
                     Slog.i(TAG, "Removing uninstalled keyboard layout " + keyboardLayout);
-                    mKeyboardLayouts.remove(i);
+                    mUnassociatedKeyboardLayouts.remove(i);
                     updateCurrentKeyboardLayoutIfRemoved(keyboardLayout, i);
                     changed = true;
                 }
@@ -428,13 +474,8 @@
                         throw new XmlPullParserException(
                                 "Missing descriptor attribute on keyboard-layout.");
                     }
-                    String current = parser.getAttributeValue(null, "current");
-                    if (mKeyboardLayouts.contains(descriptor)) {
-                        throw new XmlPullParserException(
-                                "Found duplicate keyboard layout.");
-                    }
 
-                    mKeyboardLayouts.add(descriptor);
+                    String current = parser.getAttributeValue(null, "current");
                     if (current != null && current.equals("true")) {
                         if (mCurrentKeyboardLayout != null) {
                             throw new XmlPullParserException(
@@ -442,6 +483,32 @@
                         }
                         mCurrentKeyboardLayout = descriptor;
                     }
+
+                    String inputMethodId = parser.getAttributeValue(null, "input-method-id");
+                    String inputMethodSubtypeId =
+                        parser.getAttributeValue(null, "input-method-subtype-id");
+                    if (inputMethodId == null && inputMethodSubtypeId != null
+                            || inputMethodId != null && inputMethodSubtypeId == null) {
+                        throw new XmlPullParserException(
+                                "Found an incomplete input method description");
+                    }
+
+                    if (inputMethodSubtypeId != null) {
+                        InputMethodSubtypeHandle handle = new InputMethodSubtypeHandle(
+                                inputMethodId, Integer.parseInt(inputMethodSubtypeId));
+                        if (mKeyboardLayouts.containsKey(handle)) {
+                            throw new XmlPullParserException(
+                                    "Found duplicate subtype to keyboard layout mapping: "
+                                    + handle);
+                        }
+                        mKeyboardLayouts.put(handle, descriptor);
+                    } else {
+                        if (mUnassociatedKeyboardLayouts.contains(descriptor)) {
+                            throw new XmlPullParserException(
+                                    "Found duplicate unassociated keyboard layout: " + descriptor);
+                        }
+                        mUnassociatedKeyboardLayouts.add(descriptor);
+                    }
                 } else if (parser.getName().equals("calibration")) {
                     String format = parser.getAttributeValue(null, "format");
                     String rotation = parser.getAttributeValue(null, "rotation");
@@ -492,19 +559,31 @@
             }
 
             // Maintain invariant that layouts are sorted.
-            Collections.sort(mKeyboardLayouts);
+            Collections.sort(mUnassociatedKeyboardLayouts);
 
             // Maintain invariant that there is always a current keyboard layout unless
             // there are none installed.
-            if (mCurrentKeyboardLayout == null && !mKeyboardLayouts.isEmpty()) {
-                mCurrentKeyboardLayout = mKeyboardLayouts.get(0);
+            if (mCurrentKeyboardLayout == null && !mUnassociatedKeyboardLayouts.isEmpty()) {
+                mCurrentKeyboardLayout = mUnassociatedKeyboardLayouts.get(0);
             }
         }
 
         public void saveToXml(XmlSerializer serializer) throws IOException {
-            for (String layout : mKeyboardLayouts) {
+            for (String layout : mUnassociatedKeyboardLayouts) {
                 serializer.startTag(null, "keyboard-layout");
                 serializer.attribute(null, "descriptor", layout);
+                serializer.endTag(null, "keyboard-layout");
+            }
+
+            final int N = mKeyboardLayouts.size();
+            for (int i = 0; i < N; i++) {
+                final InputMethodSubtypeHandle handle = mKeyboardLayouts.keyAt(i);
+                final String layout = mKeyboardLayouts.valueAt(i);
+                serializer.startTag(null, "keyboard-layout");
+                serializer.attribute(null, "descriptor", layout);
+                serializer.attribute(null, "input-method-id", handle.getInputMethodId());
+                serializer.attribute(null, "input-method-subtype-id",
+                        Integer.toString(handle.getSubtypeId()));
                 if (layout.equals(mCurrentKeyboardLayout)) {
                     serializer.attribute(null, "current", "true");
                 }
@@ -529,6 +608,22 @@
             }
         }
 
+        private void dump(final PrintWriter pw, final String prefix) {
+            pw.println(prefix + "CurrentKeyboardLayout=" + mCurrentKeyboardLayout);
+            pw.println(prefix + "UnassociatedKeyboardLayouts=" + mUnassociatedKeyboardLayouts);
+            pw.println(prefix + "TouchCalibration=" + Arrays.toString(mTouchCalibration));
+            pw.println(prefix + "Subtype to Layout Mappings:");
+            final int N = mKeyboardLayouts.size();
+            if (N != 0) {
+                for (int i = 0; i < N; i++) {
+                    pw.println(prefix + "  " + mKeyboardLayouts.keyAt(i) + ": "
+                            + mKeyboardLayouts.valueAt(i));
+                }
+            } else {
+                pw.println(prefix + "  <none>");
+            }
+        }
+
         private static String surfaceRotationToString(int surfaceRotation) {
             switch (surfaceRotation) {
                 case Surface.ROTATION_0:   return "0";
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 3b61817..0b1354a 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -3136,15 +3136,6 @@
             hideRecentApps(true, false);
         }
 
-        // Handle keyboard layout switching.
-        // TODO: Deprecate this behavior when we fully migrate to IME subtype-based layout rotation.
-        if (down && repeatCount == 0 && keyCode == KeyEvent.KEYCODE_SPACE
-                && ((metaState & KeyEvent.META_CTRL_MASK) != 0)) {
-            int direction = (metaState & KeyEvent.META_SHIFT_MASK) != 0 ? -1 : 1;
-            mWindowManagerFuncs.switchKeyboardLayout(event.getDeviceId(), direction);
-            return -1;
-        }
-
         // Handle input method switching.
         if (down && repeatCount == 0
                 && (keyCode == KeyEvent.KEYCODE_LANGUAGE_SWITCH
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index d2e2639..1021411 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -5308,12 +5308,6 @@
 
     // Called by window manager policy.  Not exposed externally.
     @Override
-    public void switchKeyboardLayout(int deviceId, int direction) {
-        mInputManager.switchKeyboardLayout(deviceId, direction);
-    }
-
-    // Called by window manager policy.  Not exposed externally.
-    @Override
     public void switchInputMethod(boolean forwardDirection) {
         final InputMethodManagerInternal inputMethodManagerInternal =
                 LocalServices.getService(InputMethodManagerInternal.class);