Support xml configuration file for the spell checker and add the spell checker subtype

Change-Id: I74715855525fc0a1282238d593ad37aefd42bfc3
diff --git a/api/current.txt b/api/current.txt
index a70e8a2..5a599ce 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -911,6 +911,8 @@
     field public static final int stretchMode = 16843030; // 0x1010116
     field public static final int subtitle = 16843473; // 0x10102d1
     field public static final int subtitleTextStyle = 16843513; // 0x10102f9
+    field public static final int subtypeExtraValue = 16843684; // 0x10103a4
+    field public static final int subtypeLocale = 16843683; // 0x10103a3
     field public static final int suggestActionMsg = 16843228; // 0x10101dc
     field public static final int suggestActionMsgColumn = 16843229; // 0x10101dd
     field public static final int suggestionsEnabled = 16843632; // 0x1010370
@@ -24268,6 +24270,9 @@
     method public android.content.ComponentName getComponent();
     method public java.lang.String getId();
     method public java.lang.String getPackageName();
+    method public java.lang.String getSettingsActivity();
+    method public android.view.textservice.SpellCheckerSubtype getSubtypeAt(int);
+    method public int getSubtypeCount();
     method public android.graphics.drawable.Drawable loadIcon(android.content.pm.PackageManager);
     method public java.lang.CharSequence loadLabel(android.content.pm.PackageManager);
     method public void writeToParcel(android.os.Parcel, int);
@@ -24287,6 +24292,16 @@
     method public abstract void onGetSuggestions(android.view.textservice.SuggestionsInfo[]);
   }
 
+  public final class SpellCheckerSubtype implements android.os.Parcelable {
+    ctor public SpellCheckerSubtype(int, java.lang.String, java.lang.String);
+    method public int describeContents();
+    method public java.lang.String getExtraValue();
+    method public java.lang.String getLocale();
+    method public int getNameResId();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator CREATOR;
+  }
+
   public final class SuggestionsInfo implements android.os.Parcelable {
     ctor public SuggestionsInfo(int, java.lang.String[]);
     ctor public SuggestionsInfo(int, java.lang.String[], int, int);
diff --git a/core/java/android/view/textservice/SpellCheckerInfo.java b/core/java/android/view/textservice/SpellCheckerInfo.java
index d88a39f..89cb11c 100644
--- a/core/java/android/view/textservice/SpellCheckerInfo.java
+++ b/core/java/android/view/textservice/SpellCheckerInfo.java
@@ -16,30 +16,122 @@
 
 package android.view.textservice;
 
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
 import android.graphics.drawable.Drawable;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Slog;
+import android.util.Xml;
+
+import java.io.IOException;
+import java.util.ArrayList;
 
 /**
  * This class is used to specify meta information of an spell checker.
  */
 public final class SpellCheckerInfo implements Parcelable {
+    private static final String TAG = SpellCheckerInfo.class.getSimpleName();
     private final ResolveInfo mService;
     private final String mId;
+    private final int mLabel;
+
+    /**
+     * The spell checker setting activity's name, used by the system settings to
+     * launch the setting activity.
+     */
+    private final String mSettingsActivityName;
+
+    /**
+     * The array of the subtypes.
+     */
+    private final ArrayList<SpellCheckerSubtype> mSubtypes = new ArrayList<SpellCheckerSubtype>();
 
     /**
      * Constructor.
      * @hide
      */
-    public SpellCheckerInfo(Context context, ResolveInfo service) {
+    public SpellCheckerInfo(Context context, ResolveInfo service)
+            throws XmlPullParserException, IOException {
         mService = service;
         ServiceInfo si = service.serviceInfo;
         mId = new ComponentName(si.packageName, si.name).flattenToShortString();
+
+        final PackageManager pm = context.getPackageManager();
+        int label = 0;
+        String settingsActivityComponent = null;
+        int isDefaultResId = 0;
+
+        XmlResourceParser parser = null;
+        try {
+            parser = si.loadXmlMetaData(pm, SpellCheckerSession.SERVICE_META_DATA);
+            if (parser == null) {
+                throw new XmlPullParserException("No "
+                        + SpellCheckerSession.SERVICE_META_DATA + " meta-data");
+            }
+
+            final Resources res = pm.getResourcesForApplication(si.applicationInfo);
+            final AttributeSet attrs = Xml.asAttributeSet(parser);
+            int type;
+            while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
+                    && type != XmlPullParser.START_TAG) {
+            }
+
+            final String nodeName = parser.getName();
+            if (!"spell-checker".equals(nodeName)) {
+                throw new XmlPullParserException(
+                        "Meta-data does not start with spell-checker tag");
+            }
+
+            TypedArray sa = res.obtainAttributes(attrs,
+                    com.android.internal.R.styleable.SpellChecker);
+            label = sa.getResourceId(com.android.internal.R.styleable.SpellChecker_label, 0);
+            settingsActivityComponent = sa.getString(
+                    com.android.internal.R.styleable.SpellChecker_settingsActivity);
+            sa.recycle();
+
+            final int depth = parser.getDepth();
+            // Parse all subtypes
+            while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
+                    && type != XmlPullParser.END_DOCUMENT) {
+                if (type == XmlPullParser.START_TAG) {
+                    final String subtypeNodeName = parser.getName();
+                    if (!"subtype".equals(subtypeNodeName)) {
+                        throw new XmlPullParserException(
+                                "Meta-data in spell-checker does not start with subtype tag");
+                    }
+                    final TypedArray a = res.obtainAttributes(
+                            attrs, com.android.internal.R.styleable.SpellChecker_Subtype);
+                    SpellCheckerSubtype subtype = new SpellCheckerSubtype(
+                            a.getResourceId(com.android.internal.R.styleable
+                                    .SpellChecker_Subtype_label, 0),
+                            a.getString(com.android.internal.R.styleable
+                                    .SpellChecker_Subtype_subtypeLocale),
+                            a.getString(com.android.internal.R.styleable
+                                    .SpellChecker_Subtype_subtypeExtraValue));
+                    mSubtypes.add(subtype);
+                }
+            }
+        } catch (Exception e) {
+            Slog.e(TAG, "Caught exception: " + e);
+            throw new XmlPullParserException(
+                    "Unable to create context for: " + si.packageName);
+        } finally {
+            if (parser != null) parser.close();
+        }
+        mLabel = label;
+        mSettingsActivityName = settingsActivityComponent;
     }
 
     /**
@@ -47,8 +139,11 @@
      * @hide
      */
     public SpellCheckerInfo(Parcel source) {
+        mLabel = source.readInt();
         mId = source.readString();
+        mSettingsActivityName = source.readString();
         mService = ResolveInfo.CREATOR.createFromParcel(source);
+        source.readTypedList(mSubtypes, SpellCheckerSubtype.CREATOR);
     }
 
     /**
@@ -69,7 +164,7 @@
     }
 
     /**
-     * Return the .apk package that implements this input method.
+     * Return the .apk package that implements this.
      */
     public String getPackageName() {
         return mService.serviceInfo.packageName;
@@ -83,8 +178,11 @@
      */
     @Override
     public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mLabel);
         dest.writeString(mId);
+        dest.writeString(mSettingsActivityName);
         mService.writeToParcel(dest, flags);
+        dest.writeTypedList(mSubtypes);
     }
 
 
@@ -110,7 +208,8 @@
      * @param pm Supply a PackageManager used to load the spell checker's resources.
      */
     public CharSequence loadLabel(PackageManager pm) {
-        return mService.loadLabel(pm);
+        if (mLabel == 0 || pm == null) return "";
+        return pm.getText(getPackageName(), mLabel, mService.serviceInfo.applicationInfo);
     }
 
     /**
@@ -123,6 +222,35 @@
     }
 
     /**
+     * Return the class name of an activity that provides a settings UI.
+     * You can launch this activity be starting it with
+     * an {@link android.content.Intent} whose action is MAIN and with an
+     * explicit {@link android.content.ComponentName}
+     * composed of {@link #getPackageName} and the class name returned here.
+     *
+     * <p>A null will be returned if there is no settings activity.
+     */
+    public String getSettingsActivity() {
+        return mSettingsActivityName;
+    }
+
+    /**
+     * Return the count of the subtypes.
+     */
+    public int getSubtypeCount() {
+        return mSubtypes.size();
+    }
+
+    /**
+     * Return the subtype at the specified index.
+     *
+     * @param index the index of the subtype to return.
+     */
+    public SpellCheckerSubtype getSubtypeAt(int index) {
+        return mSubtypes.get(index);
+    }
+
+    /**
      * Used to make this class parcelable.
      */
     @Override
diff --git a/core/java/android/view/textservice/SpellCheckerSubtype.aidl b/core/java/android/view/textservice/SpellCheckerSubtype.aidl
new file mode 100644
index 0000000..1c790e7
--- /dev/null
+++ b/core/java/android/view/textservice/SpellCheckerSubtype.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2011 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.textservice;
+
+parcelable SpellCheckerSubtype;
diff --git a/core/java/android/view/textservice/SpellCheckerSubtype.java b/core/java/android/view/textservice/SpellCheckerSubtype.java
new file mode 100644
index 0000000..dbd3081
--- /dev/null
+++ b/core/java/android/view/textservice/SpellCheckerSubtype.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2011 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.textservice;
+
+import android.content.Context;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * This class is used to specify meta information of a subtype contained in a spell checker.
+ * Subtype can describe locale (e.g. en_US, fr_FR...) used for settings.
+ */
+public final class SpellCheckerSubtype implements Parcelable {
+
+    private final int mSubtypeHashCode;
+    private final int mSubtypeNameResId;
+    private final String mSubtypeLocale;
+    private final String mSubtypeExtraValue;
+
+    /**
+     * Constructor
+     * @param nameId The name of the subtype
+     * @param locale The locale supported by the subtype
+     * @param extraValue The extra value of the subtype
+     */
+    public SpellCheckerSubtype(int nameId, String locale, String extraValue) {
+        mSubtypeNameResId = nameId;
+        mSubtypeLocale = locale != null ? locale : "";
+        mSubtypeExtraValue = extraValue != null ? extraValue : "";
+        mSubtypeHashCode = hashCodeInternal(mSubtypeLocale, mSubtypeExtraValue);
+    }
+
+    SpellCheckerSubtype(Parcel source) {
+        String s;
+        mSubtypeNameResId = source.readInt();
+        s = source.readString();
+        mSubtypeLocale = s != null ? s : "";
+        s = source.readString();
+        mSubtypeExtraValue = s != null ? s : "";
+        mSubtypeHashCode = hashCodeInternal(mSubtypeLocale, mSubtypeExtraValue);
+    }
+
+    /**
+     * @return the name of the subtype
+     */
+    public int getNameResId() {
+        return mSubtypeNameResId;
+    }
+
+    /**
+     * @return the locale of the subtype
+     */
+    public String getLocale() {
+        return mSubtypeLocale;
+    }
+
+    /**
+     * @return the extra value of the subtype
+     */
+    public String getExtraValue() {
+        return mSubtypeExtraValue;
+    }
+
+    @Override
+    public int hashCode() {
+        return mSubtypeHashCode;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o instanceof SpellCheckerSubtype) {
+            SpellCheckerSubtype subtype = (SpellCheckerSubtype) o;
+            return (subtype.hashCode() == hashCode())
+                && (subtype.getNameResId() == getNameResId())
+                && (subtype.getLocale().equals(getLocale()))
+                && (subtype.getExtraValue().equals(getExtraValue()));
+        }
+        return false;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int parcelableFlags) {
+        dest.writeInt(mSubtypeNameResId);
+        dest.writeString(mSubtypeLocale);
+        dest.writeString(mSubtypeExtraValue);
+    }
+
+    public static final Parcelable.Creator<SpellCheckerSubtype> CREATOR
+            = new Parcelable.Creator<SpellCheckerSubtype>() {
+        @Override
+        public SpellCheckerSubtype createFromParcel(Parcel source) {
+            return new SpellCheckerSubtype(source);
+        }
+
+        @Override
+        public SpellCheckerSubtype[] newArray(int size) {
+            return new SpellCheckerSubtype[size];
+        }
+    };
+
+    private static int hashCodeInternal(String locale, String extraValue) {
+        return Arrays.hashCode(new Object[] {locale, extraValue});
+    }
+
+    /**
+     * Sort the list of subtypes
+     * @param context Context will be used for getting localized strings
+     * @param flags Flags for the sort order
+     * @param sci SpellCheckerInfo of which subtypes are subject to be sorted
+     * @param subtypeList List which will be sorted
+     * @return Sorted list of subtypes
+     * @hide
+     */
+    public static List<SpellCheckerSubtype> sort(Context context, int flags, SpellCheckerInfo sci,
+            List<SpellCheckerSubtype> subtypeList) {
+        if (sci == null) return subtypeList;
+        final HashSet<SpellCheckerSubtype> subtypesSet = new HashSet<SpellCheckerSubtype>(
+                subtypeList);
+        final ArrayList<SpellCheckerSubtype> sortedList = new ArrayList<SpellCheckerSubtype>();
+        int N = sci.getSubtypeCount();
+        for (int i = 0; i < N; ++i) {
+            SpellCheckerSubtype subtype = sci.getSubtypeAt(i);
+            if (subtypesSet.contains(subtype)) {
+                sortedList.add(subtype);
+                subtypesSet.remove(subtype);
+            }
+        }
+        // If subtypes in subtypesSet remain, that means these subtypes are not
+        // contained in sci, so the remaining subtypes will be appended.
+        for (SpellCheckerSubtype subtype: subtypesSet) {
+            sortedList.add(subtype);
+        }
+        return sortedList;
+    }
+}
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index eed41ea..63b49bd 100755
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -2224,6 +2224,34 @@
         <attr name="imeSubtypeExtraValue" format="string" />
     </declare-styleable>
 
+    <!-- Use <code>spell-checker</code> as the root tag of the XML resource that
+         describes an
+         {@link android.service.textservice.SpellCheckerService} service, which is
+         referenced from its
+         {@link android.view.textservice.SpellCheckerSession#SERVICE_META_DATA}
+         meta-data entry.  Described here are the attributes that can be
+         included in that tag. -->
+    <declare-styleable name="SpellChecker">
+        <!-- The name of the spell checker. -->
+        <attr name="label" />
+        <!-- Component name of an activity that allows the user to modify
+             the settings for this service. -->
+        <attr name="settingsActivity"/>
+    </declare-styleable>
+
+    <!-- This is the subtype of the spell checker. Subtype can describe locales (e.g. en_US, fr_FR...) -->
+    <declare-styleable name="SpellChecker_Subtype">
+        <!-- The name of the subtype. -->
+        <attr name="label" />
+        <!-- The locale of the subtype. This string should be a locale (e.g. en_US, fr_FR...)
+             This is also used by the framework to know the supported locales
+             of the spell checker.  -->
+        <attr name="subtypeLocale" format="string" />
+        <!-- The extra value of the subtype. This string can be any string and will be passed to
+             the SpellChecker.  -->
+        <attr name="subtypeExtraValue" format="string" />
+    </declare-styleable>
+
     <!-- Use <code>accessibility-service</code> as the root tag of the XML resource that
          describes an {@link android.accessibilityservice.AccessibilityService} service,
          which is referenced from its
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index f85dd85..1ba54cf 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -1785,6 +1785,9 @@
   <public type="attr" name="actionBarWidgetTheme" />
   <public type="attr" name="uiOptions" />
 
+  <public type="attr" name="subtypeLocale" />
+  <public type="attr" name="subtypeExtraValue" />
+
   <public type="style" name="TextAppearance.SuggestionHighlight" />
 
   <public type="style" name="Theme.Holo.Light.DarkActionBar" />
diff --git a/services/java/com/android/server/TextServicesManagerService.java b/services/java/com/android/server/TextServicesManagerService.java
index 238b747..90824a6 100644
--- a/services/java/com/android/server/TextServicesManagerService.java
+++ b/services/java/com/android/server/TextServicesManagerService.java
@@ -23,6 +23,8 @@
 import com.android.internal.textservice.ITextServicesManager;
 import com.android.internal.textservice.ITextServicesSessionListener;
 
+import org.xmlpull.v1.XmlPullParserException;
+
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -40,6 +42,7 @@
 import android.util.Slog;
 import android.view.textservice.SpellCheckerInfo;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -125,9 +128,15 @@
                 continue;
             }
             if (DBG) Slog.d(TAG, "Add: " + compName);
-            final SpellCheckerInfo sci = new SpellCheckerInfo(context, ri);
-            list.add(sci);
-            map.put(sci.getId(), sci);
+            try {
+                final SpellCheckerInfo sci = new SpellCheckerInfo(context, ri);
+                list.add(sci);
+                map.put(sci.getId(), sci);
+            } catch (XmlPullParserException e) {
+                Slog.w(TAG, "Unable to load the spell checker " + compName, e);
+            } catch (IOException e) {
+                Slog.w(TAG, "Unable to load the spell checker " + compName, e);
+            }
         }
         if (DBG) {
             Slog.d(TAG, "buildSpellCheckerMapLocked: " + list.size() + "," + map.size());