Support loading keyboard layout overlays from resources.

Added the concept of a keyboard layout overlay, which is
a key character map file that has "type OVERLAY".

Added support for loading keyboard layout overlays from
resources dynamically.  The layouts are reloaded whenever they
are changed in the Settings application or an application
is installed.  This is somewhat more aggressive than necessary
so we might want to optimize it later.

Before system-ready, the input system uses just the generic
keyboard layouts that are included on the device system image.
After system-ready, it considers the user's selected keyboard
layout overlay and attempts to load it as necessary.  We need to
wait until system-ready before doing this because we need to
be in a state where it is safe to start applications or access
their resources.

Bug: 6110399
Change-Id: Iae0886d3356649b0d2440aa00910a888cedd8323
diff --git a/services/java/com/android/server/input/InputManagerService.java b/services/java/com/android/server/input/InputManagerService.java
index 7640eff..a4ed31c 100644
--- a/services/java/com/android/server/input/InputManagerService.java
+++ b/services/java/com/android/server/input/InputManagerService.java
@@ -26,15 +26,18 @@
 import org.xmlpull.v1.XmlSerializer;
 
 import android.Manifest;
+import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Configuration;
 import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
 import android.database.ContentObserver;
@@ -60,13 +63,11 @@
 import android.view.InputChannel;
 import android.view.InputDevice;
 import android.view.InputEvent;
-import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
 import android.view.PointerIcon;
 import android.view.Surface;
 import android.view.ViewConfiguration;
 import android.view.WindowManagerPolicy;
-import android.view.KeyCharacterMap.UnavailableException;
 
 import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
@@ -77,13 +78,14 @@
 import java.io.FileReader;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 
 import libcore.io.IoUtils;
+import libcore.io.Streams;
 import libcore.util.Objects;
 
 /*
@@ -91,7 +93,7 @@
  */
 public class InputManagerService extends IInputManager.Stub implements Watchdog.Monitor {
     static final String TAG = "InputManager";
-    static final boolean DEBUG = false;
+    static final boolean DEBUG = true;
 
     private static final String EXCLUDED_DEVICES_PATH = "etc/excluded-input-devices.xml";
 
@@ -103,6 +105,7 @@
     private final Context mContext;
     private final Callbacks mCallbacks;
     private final InputManagerHandler mHandler;
+    private boolean mSystemReady;
 
     // Persistent data store.  Must be locked each time during use.
     private final PersistentDataStore mDataStore = new PersistentDataStore();
@@ -163,6 +166,7 @@
     private static native void nativeVibrate(int ptr, int deviceId, long[] pattern,
             int repeat, int token);
     private static native void nativeCancelVibrate(int ptr, int deviceId, int token);
+    private static native void nativeReloadKeyboardLayouts(int ptr);
     private static native String nativeDump(int ptr);
     private static native void nativeMonitor(int ptr);
 
@@ -212,7 +216,33 @@
         updatePointerSpeedFromSettings();
         updateShowTouchesFromSettings();
     }
-    
+
+    public void systemReady() {
+        if (DEBUG) {
+            Slog.d(TAG, "System ready.");
+        }
+        mSystemReady = true;
+        reloadKeyboardLayouts();
+
+        IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
+        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+        filter.addDataScheme("package");
+        mContext.registerReceiver(new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                if (DEBUG) {
+                    Slog.d(TAG, "Packages changed, reloading keyboard layouts.");
+                }
+                reloadKeyboardLayouts();
+            }
+        }, filter, null, mHandler);
+    }
+
+    private void reloadKeyboardLayouts() {
+        nativeReloadKeyboardLayouts(mPtr);
+    }
+
     public void setDisplaySize(int displayId, int width, int height,
             int externalWidth, int externalHeight) {
         if (width <= 0 || height <= 0 || externalWidth <= 0 || externalHeight <= 0) {
@@ -528,14 +558,14 @@
 
     @Override // Binder call
     public KeyboardLayout[] getKeyboardLayouts() {
-        ArrayList<KeyboardLayout> list = new ArrayList<KeyboardLayout>();
-
-        final PackageManager pm = mContext.getPackageManager();
-        Intent intent = new Intent(InputManager.ACTION_QUERY_KEYBOARD_LAYOUTS);
-        for (ResolveInfo resolveInfo : pm.queryBroadcastReceivers(intent,
-                PackageManager.GET_META_DATA)) {
-            loadKeyboardLayouts(pm, resolveInfo.activityInfo, list, null);
-        }
+        final ArrayList<KeyboardLayout> list = new ArrayList<KeyboardLayout>();
+        visitAllKeyboardLayouts(new KeyboardLayoutVisitor() {
+            @Override
+            public void visitKeyboardLayout(Resources resources,
+                    String descriptor, String label, int kcmResId) {
+                list.add(new KeyboardLayout(descriptor, label));
+            }
+        });
         return list.toArray(new KeyboardLayout[list.size()]);
     }
 
@@ -545,37 +575,57 @@
             throw new IllegalArgumentException("keyboardLayoutDescriptor must not be null");
         }
 
-        KeyboardLayoutDescriptor d = KeyboardLayoutDescriptor.parse(keyboardLayoutDescriptor);
-        if (d == null) {
-            return null;
+        final KeyboardLayout[] result = new KeyboardLayout[1];
+        visitKeyboardLayout(keyboardLayoutDescriptor, new KeyboardLayoutVisitor() {
+            @Override
+            public void visitKeyboardLayout(Resources resources,
+                    String descriptor, String label, int kcmResId) {
+                result[0] = new KeyboardLayout(descriptor, label);
+            }
+        });
+        if (result[0] == null) {
+            Log.w(TAG, "Could not get keyboard layout with descriptor '"
+                    + keyboardLayoutDescriptor + "'.");
         }
+        return result[0];
+    }
 
+    private void visitAllKeyboardLayouts(KeyboardLayoutVisitor visitor) {
         final PackageManager pm = mContext.getPackageManager();
-        try {
-            ActivityInfo receiver = pm.getReceiverInfo(
-                    new ComponentName(d.packageName, d.receiverName),
-                    PackageManager.GET_META_DATA);
-            return loadKeyboardLayouts(pm, receiver, null, d.keyboardLayoutName);
-        } catch (NameNotFoundException ex) {
-            Log.w(TAG, "Could not load keyboard layout '" + d.keyboardLayoutName
-                    + "' from receiver " + d.packageName + "/" + d.receiverName, ex);
-            return null;
+        Intent intent = new Intent(InputManager.ACTION_QUERY_KEYBOARD_LAYOUTS);
+        for (ResolveInfo resolveInfo : pm.queryBroadcastReceivers(intent,
+                PackageManager.GET_META_DATA)) {
+            visitKeyboardLayoutsInPackage(pm, resolveInfo.activityInfo, null, visitor);
         }
     }
 
-    private KeyboardLayout loadKeyboardLayouts(
-            PackageManager pm, ActivityInfo receiver,
-            List<KeyboardLayout> list, String keyboardName) {
+    private void visitKeyboardLayout(String keyboardLayoutDescriptor,
+            KeyboardLayoutVisitor visitor) {
+        KeyboardLayoutDescriptor d = KeyboardLayoutDescriptor.parse(keyboardLayoutDescriptor);
+        if (d != null) {
+            final PackageManager pm = mContext.getPackageManager();
+            try {
+                ActivityInfo receiver = pm.getReceiverInfo(
+                        new ComponentName(d.packageName, d.receiverName),
+                        PackageManager.GET_META_DATA);
+                visitKeyboardLayoutsInPackage(pm, receiver, d.keyboardLayoutName, visitor);
+            } catch (NameNotFoundException ex) {
+            }
+        }
+    }
+
+    private void visitKeyboardLayoutsInPackage(PackageManager pm, ActivityInfo receiver,
+            String keyboardName, KeyboardLayoutVisitor visitor) {
         Bundle metaData = receiver.metaData;
         if (metaData == null) {
-            return null;
+            return;
         }
 
         int configResId = metaData.getInt(InputManager.META_DATA_KEYBOARD_LAYOUTS);
         if (configResId == 0) {
             Log.w(TAG, "Missing meta-data '" + InputManager.META_DATA_KEYBOARD_LAYOUTS
                     + "' on receiver " + receiver.packageName + "/" + receiver.name);
-            return null;
+            return;
         }
 
         try {
@@ -608,12 +658,9 @@
                             } else {
                                 String descriptor = KeyboardLayoutDescriptor.format(
                                         receiver.packageName, receiver.name, name);
-                                KeyboardLayout c = new KeyboardLayout(descriptor, label);
-                                if (keyboardName != null && name.equals(keyboardName)) {
-                                    return c;
-                                }
-                                if (list != null) {
-                                    list.add(c);
+                                if (keyboardName == null || name.equals(keyboardName)) {
+                                    visitor.visitKeyboardLayout(resources, descriptor,
+                                            label, kcmResId);
                                 }
                             }
                         } finally {
@@ -629,16 +676,9 @@
                 parser.close();
             }
         } catch (Exception ex) {
-            Log.w(TAG, "Could not load keyboard layout resource from receiver "
+            Log.w(TAG, "Could not parse keyboard layout resource from receiver "
                     + receiver.packageName + "/" + receiver.name, ex);
-            return null;
         }
-        if (keyboardName != null) {
-            Log.w(TAG, "Could not load keyboard layout '" + keyboardName
-                    + "' from receiver " + receiver.packageName + "/" + receiver.name
-                    + " because it was not declared in the keyboard layout resource.");
-        }
-        return null;
     }
 
     @Override // Binder call
@@ -664,52 +704,24 @@
             throw new IllegalArgumentException("inputDeviceDescriptor must not be null");
         }
 
+        final boolean changed;
         synchronized (mDataStore) {
             try {
-                mDataStore.setKeyboardLayout(inputDeviceDescriptor, keyboardLayoutDescriptor);
+                changed = mDataStore.setKeyboardLayout(
+                        inputDeviceDescriptor, keyboardLayoutDescriptor);
             } finally {
                 mDataStore.saveIfNeeded();
             }
         }
+
+        if (changed) {
+            if (DEBUG) {
+                Slog.d(TAG, "Keyboard layout changed, reloading keyboard layouts.");
+            }
+            reloadKeyboardLayouts();
+        }
     }
 
-    /**
-     * Loads the key character map associated with the keyboard layout.
-     *
-     * @param pm The package manager.
-     * @return The key character map, or null if it could not be loaded for any reason.
-     *
-    public KeyCharacterMap loadKeyCharacterMap(PackageManager pm) {
-        if (pm == null) {
-            throw new IllegalArgumentException("pm must not be null");
-        }
-
-        if (mKeyCharacterMap == null) {
-            KeyboardLayoutDescriptor d = InputManager.parseKeyboardLayoutDescriptor(mDescriptor);
-            if (d == null) {
-                Log.e(InputManager.TAG, "Could not load key character map '" + mDescriptor
-                        + "' because the descriptor could not be parsed.");
-                return null;
-            }
-
-            CharSequence cs = pm.getText(d.packageName, mKeyCharacterMapResId, null);
-            if (cs == null) {
-                Log.e(InputManager.TAG, "Could not load key character map '" + mDescriptor
-                        + "' because its associated resource could not be loaded.");
-                return null;
-            }
-
-            try {
-                mKeyCharacterMap = KeyCharacterMap.load(cs);
-            } catch (UnavailableException ex) {
-                Log.e(InputManager.TAG, "Could not load key character map '" + mDescriptor
-                        + "' due to an error while parsing.", ex);
-                return null;
-            }
-        }
-        return mKeyCharacterMap;
-    }*/
-
     public void setInputWindows(InputWindowHandle[] windowHandles) {
         nativeSetInputWindows(mPtr, windowHandles);
     }
@@ -1076,6 +1088,40 @@
         return PointerIcon.getDefaultIcon(mContext);
     }
 
+    // Native callback.
+    private String[] getKeyboardLayoutOverlay(String inputDeviceDescriptor) {
+        if (!mSystemReady) {
+            return null;
+        }
+
+        String keyboardLayoutDescriptor = getKeyboardLayoutForInputDevice(inputDeviceDescriptor);
+        if (keyboardLayoutDescriptor == null) {
+            return null;
+        }
+
+        final String[] result = new String[2];
+        visitKeyboardLayout(keyboardLayoutDescriptor, new KeyboardLayoutVisitor() {
+            @Override
+            public void visitKeyboardLayout(Resources resources,
+                    String descriptor, String label, int kcmResId) {
+                try {
+                    result[0] = descriptor;
+                    result[1] = Streams.readFully(new InputStreamReader(
+                            resources.openRawResource(kcmResId)));
+                } catch (IOException ex) {
+                } catch (NotFoundException ex) {
+                }
+            }
+        });
+        if (result[0] == null) {
+            Log.w(TAG, "Could not get keyboard layout with descriptor '"
+                    + keyboardLayoutDescriptor + "'.");
+            return null;
+        }
+        return result;
+    }
+
+
     /**
      * Callback interface implemented by the Window Manager.
      */
@@ -1169,6 +1215,11 @@
         }
     }
 
+    private interface KeyboardLayoutVisitor {
+        void visitKeyboardLayout(Resources resources,
+                String descriptor, String label, int kcmResId);
+    }
+
     private final class InputDevicesChangedListenerRecord implements DeathRecipient {
         private final int mPid;
         private final IInputDevicesChangedListener mListener;