API to set additional subtypes

Bug: 4321667

- Due to the voice team's strong request

Change-Id: Ia8bbc277c0987bde996e6f65541877cec96a3ca1
diff --git a/services/java/com/android/server/InputMethodManagerService.java b/services/java/com/android/server/InputMethodManagerService.java
index f4308cd..fbde9d1 100644
--- a/services/java/com/android/server/InputMethodManagerService.java
+++ b/services/java/com/android/server/InputMethodManagerService.java
@@ -17,7 +17,9 @@
 package com.android.server;
 
 import com.android.internal.content.PackageMonitor;
+import com.android.internal.os.AtomicFile;
 import com.android.internal.os.HandlerCaller;
+import com.android.internal.util.FastXmlSerializer;
 import com.android.internal.view.IInputContext;
 import com.android.internal.view.IInputMethod;
 import com.android.internal.view.IInputMethodCallback;
@@ -25,10 +27,11 @@
 import com.android.internal.view.IInputMethodManager;
 import com.android.internal.view.IInputMethodSession;
 import com.android.internal.view.InputBindResult;
-
 import com.android.server.EventLogTags;
 
+import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
 
 import android.app.ActivityManagerNative;
 import android.app.AlertDialog;
@@ -37,9 +40,9 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.DialogInterface;
-import android.content.IntentFilter;
 import android.content.DialogInterface.OnCancelListener;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.ServiceConnection;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
@@ -51,6 +54,7 @@
 import android.database.ContentObserver;
 import android.inputmethodservice.InputMethodService;
 import android.os.Binder;
+import android.os.Environment;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.IInterface;
@@ -68,9 +72,10 @@
 import android.util.EventLog;
 import android.util.LruCache;
 import android.util.Pair;
-import android.util.Slog;
 import android.util.PrintWriterPrinter;
 import android.util.Printer;
+import android.util.Slog;
+import android.util.Xml;
 import android.view.IWindowManager;
 import android.view.WindowManager;
 import android.view.inputmethod.EditorInfo;
@@ -80,15 +85,19 @@
 import android.view.inputmethod.InputMethodManager;
 import android.view.inputmethod.InputMethodSubtype;
 
+import java.io.File;
 import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
+import java.util.Set;
 import java.util.TreeMap;
 
 /**
@@ -134,6 +143,7 @@
     final StatusBarManagerService mStatusBar;
     final IWindowManager mIWindowManager;
     final HandlerCaller mCaller;
+    private final InputMethodFileManager mFileManager;
 
     final InputBindResult mNoBinding = new InputBindResult(null, null, -1);
 
@@ -407,10 +417,15 @@
                 if (curInputMethodId != null) {
                     for (int i=0; i<N; i++) {
                         InputMethodInfo imi = mMethodList.get(i);
-                        if (imi.getId().equals(curInputMethodId)) {
+                        final String imiId = imi.getId();
+                        if (imiId.equals(curInputMethodId)) {
                             curIm = imi;
                         }
+
                         int change = isPackageDisappearing(imi.getPackageName());
+                        if (isPackageModified(imi.getPackageName())) {
+                            mFileManager.deleteAllInputMethodSubtypes(imiId);
+                        }
                         if (change == PACKAGE_TEMPORARY_CHANGE
                                 || change == PACKAGE_PERMANENT_CHANGE) {
                             Slog.i(TAG, "Input method uninstalled, disabling: "
@@ -471,9 +486,11 @@
             mMethod = method;
         }
 
+        @Override
         public void finishedEvent(int seq, boolean handled) throws RemoteException {
         }
 
+        @Override
         public void sessionCreated(IInputMethodSession session) throws RemoteException {
             onSessionCreated(mMethod, session);
         }
@@ -486,10 +503,14 @@
         mIWindowManager = IWindowManager.Stub.asInterface(
                 ServiceManager.getService(Context.WINDOW_SERVICE));
         mCaller = new HandlerCaller(context, new HandlerCaller.Callback() {
+            @Override
             public void executeMessage(Message msg) {
                 handleMessage(msg);
             }
         });
+        synchronized (mMethodMap) {
+            mFileManager = new InputMethodFileManager(mMethodMap);
+        }
 
         (new MyPackageMonitor()).register(mContext, true);
 
@@ -566,12 +587,14 @@
         }
     }
 
+    @Override
     public List<InputMethodInfo> getInputMethodList() {
         synchronized (mMethodMap) {
             return new ArrayList<InputMethodInfo>(mMethodList);
         }
     }
 
+    @Override
     public List<InputMethodInfo> getEnabledInputMethodList() {
         synchronized (mMethodMap) {
             return mSettings.getEnabledInputMethodListLocked();
@@ -602,6 +625,7 @@
         return InputMethodSubtype.sort(mContext, 0, imi, enabledSubtypes);
     }
 
+    @Override
     public List<InputMethodSubtype> getEnabledInputMethodSubtypeList(InputMethodInfo imi,
             boolean allowsImplicitlySelectedSubtypes) {
         synchronized (mMethodMap) {
@@ -609,6 +633,7 @@
         }
     }
 
+    @Override
     public void addClient(IInputMethodClient client,
             IInputContext inputContext, int uid, int pid) {
         synchronized (mMethodMap) {
@@ -617,6 +642,7 @@
         }
     }
 
+    @Override
     public void removeClient(IInputMethodClient client) {
         synchronized (mMethodMap) {
             mClients.remove(client.asBinder());
@@ -840,6 +866,7 @@
         return null;
     }
 
+    @Override
     public InputBindResult startInput(IInputMethodClient client,
             IInputContext inputContext, EditorInfo attribute,
             boolean initial, boolean needResult) {
@@ -854,9 +881,11 @@
         }
     }
 
+    @Override
     public void finishInput(IInputMethodClient client) {
     }
 
+    @Override
     public void onServiceConnected(ComponentName name, IBinder service) {
         synchronized (mMethodMap) {
             if (mCurIntent != null && name.equals(mCurIntent.getComponent())) {
@@ -947,6 +976,7 @@
         mStatusBar.setIconVisibility("ime", false);
     }
 
+    @Override
     public void onServiceDisconnected(ComponentName name) {
         synchronized (mMethodMap) {
             if (DEBUG) Slog.v(TAG, "Service disconnected: " + name
@@ -1012,6 +1042,7 @@
         }
     }
 
+    @Override
     public void registerSuggestionSpansForNotification(SuggestionSpan[] spans) {
         synchronized (mMethodMap) {
             final InputMethodInfo currentImi = mMethodMap.get(mCurMethodId);
@@ -1025,6 +1056,7 @@
         }
     }
 
+    @Override
     public boolean notifySuggestionPicked(SuggestionSpan span, String originalString, int index) {
         synchronized (mMethodMap) {
             final InputMethodInfo targetImi = mSecureSuggestionSpans.get(span);
@@ -1485,6 +1517,7 @@
         }
     }
 
+    @Override
     public InputMethodSubtype getLastInputMethodSubtype() {
         synchronized (mMethodMap) {
             final Pair<String, String> lastIme = mSettings.getLastInputMethodAndSubtypeLocked();
@@ -1503,6 +1536,22 @@
         }
     }
 
+    @Override
+    public boolean setAdditionalInputMethodSubtypes(IBinder token, InputMethodSubtype[] subtypes) {
+        if (token == null || mCurToken != token) {
+            return false;
+        }
+        if (subtypes == null || subtypes.length == 0) return false;
+        synchronized (mMethodMap) {
+            final InputMethodInfo imi = mMethodMap.get(mCurMethodId);
+            if (imi == null) return false;
+            final int N = subtypes.length;
+            mFileManager.addInputMethodSubtypes(mCurMethodId, subtypes);
+            buildInputMethodListLocked(mMethodList, mMethodMap);
+            return true;
+        }
+    }
+
     private void setInputMethodWithSubtypeId(IBinder token, String id, int subtypeId) {
         synchronized (mMethodMap) {
             if (token == null) {
@@ -1749,6 +1798,8 @@
                 new Intent(InputMethod.SERVICE_INTERFACE),
                 PackageManager.GET_META_DATA);
 
+        final HashMap<String, List<InputMethodSubtype>> additionalSubtypes =
+                mFileManager.getAllAdditionalInputMethodSubtypes();
         for (int i = 0; i < services.size(); ++i) {
             ResolveInfo ri = services.get(i);
             ServiceInfo si = ri.serviceInfo;
@@ -1764,7 +1815,7 @@
             if (DEBUG) Slog.d(TAG, "Checking " + compName);
 
             try {
-                InputMethodInfo p = new InputMethodInfo(mContext, ri);
+                InputMethodInfo p = new InputMethodInfo(mContext, ri, additionalSubtypes);
                 list.add(p);
                 final String id = p.getId();
                 map.put(id, p);
@@ -2399,6 +2450,7 @@
     }
 
     // TODO: We should change the return type from List to List<Parcelable>
+    @Override
     public List getShortcutInputMethodsAndSubtypes() {
         synchronized (mMethodMap) {
             ArrayList<Object> ret = new ArrayList<Object>();
@@ -2821,6 +2873,200 @@
         }
     }
 
+    private static class InputMethodFileManager {
+        private static final String SYSTEM_PATH = "system";
+        private static final String INPUT_METHOD_PATH = "inputmethod";
+        private static final String ADDITIONAL_SUBTYPES_FILE_NAME = "subtypes.xml";
+        private static final String NODE_SUBTYPES = "subtypes";
+        private static final String NODE_SUBTYPE = "subtype";
+        private static final String NODE_IMI = "imi";
+        private static final String ATTR_ID = "id";
+        private static final String ATTR_LABEL = "label";
+        private static final String ATTR_ICON = "icon";
+        private static final String ATTR_IME_SUBTYPE_LOCALE = "imeSubtypeLocale";
+        private static final String ATTR_IME_SUBTYPE_MODE = "imeSubtypeMode";
+        private static final String ATTR_IME_SUBTYPE_EXTRA_VALUE = "imeSubtypeExtraValue";
+        private static final String ATTR_IS_AUXILIARY = "isAuxiliary";
+        private final AtomicFile mAdditionalInputMethodSubtypeFile;
+        private final HashMap<String, InputMethodInfo> mMethodMap;
+        private final HashMap<String, List<InputMethodSubtype>> mSubtypesMap =
+                new HashMap<String, List<InputMethodSubtype>>();
+        public InputMethodFileManager(HashMap<String, InputMethodInfo> methodMap) {
+            if (methodMap == null) {
+                throw new NullPointerException("methodMap is null");
+            }
+            mMethodMap = methodMap;
+            final File systemDir = new File(Environment.getDataDirectory(), SYSTEM_PATH);
+            final File inputMethodDir = new File(systemDir, INPUT_METHOD_PATH);
+            if (!inputMethodDir.mkdirs()) {
+                Slog.w(TAG, "Couldn't create dir.: " + inputMethodDir.getAbsolutePath());
+            }
+            final File subtypeFile = new File(inputMethodDir, ADDITIONAL_SUBTYPES_FILE_NAME);
+            mAdditionalInputMethodSubtypeFile = new AtomicFile(subtypeFile);
+            if (!subtypeFile.exists()) {
+                // If "subtypes.xml" doesn't exist, create a blank file.
+                writeAdditionalInputMethodSubtypes(mSubtypesMap, mAdditionalInputMethodSubtypeFile,
+                        methodMap);
+            } else {
+                readAdditionalInputMethodSubtypes(mSubtypesMap, mAdditionalInputMethodSubtypeFile);
+            }
+        }
+
+        private void deleteAllInputMethodSubtypes(String imiId) {
+            synchronized (mMethodMap) {
+                mSubtypesMap.remove(imiId);
+                writeAdditionalInputMethodSubtypes(mSubtypesMap, mAdditionalInputMethodSubtypeFile,
+                        mMethodMap);
+            }
+        }
+
+        public void addInputMethodSubtypes(
+                String imiId, InputMethodSubtype[] additionalSubtypes) {
+            synchronized (mMethodMap) {
+                final ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>();
+                final int N = additionalSubtypes.length;
+                for (int i = 0; i < N; ++i) {
+                    final InputMethodSubtype subtype = additionalSubtypes[i];
+                    if (!subtypes.contains(subtype)) {
+                        subtypes.add(subtype);
+                    }
+                }
+                mSubtypesMap.put(imiId, subtypes);
+                writeAdditionalInputMethodSubtypes(mSubtypesMap, mAdditionalInputMethodSubtypeFile,
+                        mMethodMap);
+            }
+        }
+
+        public HashMap<String, List<InputMethodSubtype>> getAllAdditionalInputMethodSubtypes() {
+            synchronized (mMethodMap) {
+                return mSubtypesMap;
+            }
+        }
+
+        private static void writeAdditionalInputMethodSubtypes(
+                HashMap<String, List<InputMethodSubtype>> allSubtypes, AtomicFile subtypesFile,
+                HashMap<String, InputMethodInfo> methodMap) {
+            // Safety net for the case that this function is called before methodMap is set.
+            final boolean isSetMethodMap = methodMap != null && methodMap.size() > 0;
+            FileOutputStream fos = null;
+            try {
+                fos = subtypesFile.startWrite();
+                final XmlSerializer out = new FastXmlSerializer();
+                out.setOutput(fos, "utf-8");
+                out.startDocument(null, true);
+                out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+                out.startTag(null, NODE_SUBTYPES);
+                for (String imiId : allSubtypes.keySet()) {
+                    if (isSetMethodMap && !methodMap.containsKey(imiId)) {
+                        Slog.w(TAG, "IME uninstalled or not valid.: " + imiId);
+                        continue;
+                    }
+                    out.startTag(null, NODE_IMI);
+                    out.attribute(null, ATTR_ID, imiId);
+                    final List<InputMethodSubtype> subtypesList = allSubtypes.get(imiId);
+                    final int N = subtypesList.size();
+                    for (int i = 0; i < N; ++i) {
+                        final InputMethodSubtype subtype = subtypesList.get(i);
+                        out.startTag(null, NODE_SUBTYPE);
+                        out.attribute(null, ATTR_ICON, String.valueOf(subtype.getIconResId()));
+                        out.attribute(null, ATTR_LABEL, String.valueOf(subtype.getNameResId()));
+                        out.attribute(null, ATTR_IME_SUBTYPE_LOCALE, subtype.getLocale());
+                        out.attribute(null, ATTR_IME_SUBTYPE_MODE, subtype.getMode());
+                        out.attribute(null, ATTR_IME_SUBTYPE_EXTRA_VALUE, subtype.getExtraValue());
+                        out.attribute(null, ATTR_IS_AUXILIARY,
+                                String.valueOf(subtype.isAuxiliary() ? 1 : 0));
+                        out.endTag(null, NODE_SUBTYPE);
+                    }
+                    out.endTag(null, NODE_IMI);
+                }
+                out.endTag(null, NODE_SUBTYPES);
+                out.endDocument();
+                subtypesFile.finishWrite(fos);
+            } catch (java.io.IOException e) {
+                Slog.w(TAG, "Error writing subtypes", e);
+                if (fos != null) {
+                    subtypesFile.failWrite(fos);
+                }
+            }
+        }
+
+        private static void readAdditionalInputMethodSubtypes(
+                HashMap<String, List<InputMethodSubtype>> allSubtypes, AtomicFile subtypesFile) {
+            if (allSubtypes == null || subtypesFile == null) return;
+            allSubtypes.clear();
+            FileInputStream fis = null;
+            try {
+                fis = subtypesFile.openRead();
+                final XmlPullParser parser = Xml.newPullParser();
+                parser.setInput(fis, null);
+                int type = parser.getEventType();
+                // Skip parsing until START_TAG
+                while ((type = parser.next()) != XmlPullParser.START_TAG
+                        && type != XmlPullParser.END_DOCUMENT) {}
+                String firstNodeName = parser.getName();
+                if (!NODE_SUBTYPES.equals(firstNodeName)) {
+                    throw new XmlPullParserException("Xml doesn't start with subtypes");
+                }
+                final int depth =parser.getDepth();
+                String currentImiId = null;
+                ArrayList<InputMethodSubtype> tempSubtypesArray = null;
+                while (((type = parser.next()) != XmlPullParser.END_TAG
+                        || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
+                    if (type != XmlPullParser.START_TAG)
+                        continue;
+                    final String nodeName = parser.getName();
+                    if (NODE_IMI.equals(nodeName)) {
+                        currentImiId = parser.getAttributeValue(null, ATTR_ID);
+                        if (TextUtils.isEmpty(currentImiId)) {
+                            Slog.w(TAG, "Invalid imi id found in subtypes.xml");
+                            continue;
+                        }
+                        tempSubtypesArray = new ArrayList<InputMethodSubtype>();
+                        allSubtypes.put(currentImiId, tempSubtypesArray);
+                    } else if (NODE_SUBTYPE.equals(nodeName)) {
+                        if (TextUtils.isEmpty(currentImiId) || tempSubtypesArray == null) {
+                            Slog.w(TAG, "IME uninstalled or not valid.: " + currentImiId);
+                            continue;
+                        }
+                        final int icon = Integer.valueOf(
+                                parser.getAttributeValue(null, ATTR_ICON));
+                        final int label = Integer.valueOf(
+                                parser.getAttributeValue(null, ATTR_LABEL));
+                        final String imeSubtypeLocale =
+                                parser.getAttributeValue(null, ATTR_IME_SUBTYPE_LOCALE);
+                        final String imeSubtypeMode =
+                                parser.getAttributeValue(null, ATTR_IME_SUBTYPE_MODE);
+                        final String imeSubtypeExtraValue =
+                                parser.getAttributeValue(null, ATTR_IME_SUBTYPE_EXTRA_VALUE);
+                        final boolean isAuxiliary =
+                                Boolean.valueOf(parser.getAttributeValue(null, ATTR_IS_AUXILIARY));
+                        final InputMethodSubtype subtype =
+                                new InputMethodSubtype(label, icon, imeSubtypeLocale,
+                                        imeSubtypeMode, imeSubtypeExtraValue, isAuxiliary);
+                        tempSubtypesArray.add(subtype);
+                    }
+                }
+            } catch (XmlPullParserException e) {
+                Slog.w(TAG, "Error reading subtypes: " + e);
+                return;
+            } catch (java.io.IOException e) {
+                Slog.w(TAG, "Error reading subtypes: " + e);
+                return;
+            } catch (NumberFormatException e) {
+                Slog.w(TAG, "Error reading subtypes: " + e);
+                return;
+            } finally {
+                if (fis != null) {
+                    try {
+                        fis.close();
+                    } catch (java.io.IOException e1) {
+                        Slog.w(TAG, "Failed to close.");
+                    }
+                }
+            }
+        }
+    }
+
     // ----------------------------------------------------------------------
 
     @Override