Add CorrectionSpan and APIs to pass a secure CorrectionSpan to TextView

- CorrectionSpan is a span which has suggestions made by IME.
This has a function to change the current IME to other IME specified
in this span. For security reasons, only the current IME
is allowed to use this function through InputConnection.
(IME token is used for checking the validity of it.).

- CorrectionSpan stores following information:

flags, subtype Id, InputMethodInfo Id, suggests, locale, original string

Change-Id: Id3abc9ea4d11753cdc4f483a2bb3128f49ba198a
diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java
index 30a1f48..ee6342a 100644
--- a/core/java/android/text/TextUtils.java
+++ b/core/java/android/text/TextUtils.java
@@ -565,6 +565,8 @@
     public static final int TEXT_APPEARANCE_SPAN = 17;
     /** @hide */
     public static final int ANNOTATION = 18;
+    /** @hide */
+    public static final int CORRECTION_SPAN = 19;
 
     /**
      * Flatten a CharSequence and whatever styles can be copied across processes
diff --git a/core/java/android/text/style/CorrectionSpan.aidl b/core/java/android/text/style/CorrectionSpan.aidl
new file mode 100644
index 0000000..82e3d04
--- /dev/null
+++ b/core/java/android/text/style/CorrectionSpan.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.text.style;
+
+parcelable CorrectionSpan;
diff --git a/core/java/android/text/style/CorrectionSpan.java b/core/java/android/text/style/CorrectionSpan.java
new file mode 100644
index 0000000..6142e6f
--- /dev/null
+++ b/core/java/android/text/style/CorrectionSpan.java
@@ -0,0 +1,161 @@
+/*
+ * 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.text.style;
+
+import android.content.Context;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.ParcelableSpan;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+public class CorrectionSpan implements ParcelableSpan {
+
+    /**
+     * Flag for the default value.
+     */
+    public static final int FLAG_DEFAULT = 0x0000;
+    /**
+     * Flag for indicating that the input is verbatim. TextView refers to this flag to determine
+     * how it displays a word with CorrectionSpan.
+     */
+    public static final int FLAG_VERBATIM = 0x0001;
+
+    private static final int SUGGESTS_MAX_SIZE = 5;
+
+    /*
+     * TODO: Needs to check the validity and add a feature that TextView will change
+     * the current IME to the other IME which is specified in CorrectionSpan.
+     * An IME needs to set the span by specifying the target IME and Subtype of CorrectionSpan.
+     * And the current IME might want to specify any IME as the target IME including other IMEs.
+     */
+
+    private final int mFlags;
+    private final List<CharSequence> mSuggests = new ArrayList<CharSequence>();
+    private final String mLocaleString;
+    private final String mOriginalString;
+    /*
+     * TODO: If switching IME is required, needs to add parameters for ids of InputMethodInfo
+     * and InputMethodSubtype.
+     */
+
+    /**
+     * @param context Context for the application
+     * @param suggests Suggests for the string under the span
+     * @param flags Additional flags indicating how this span is handled in TextView
+     */
+    public CorrectionSpan(Context context, List<CharSequence> suggests, int flags) {
+        this(context, null, suggests, flags, null);
+    }
+
+    /**
+     * @param locale Locale of the suggestions
+     * @param suggests Suggests for the string under the span
+     * @param flags Additional flags indicating how this span is handled in TextView
+     */
+    public CorrectionSpan(Locale locale, List<CharSequence> suggests, int flags) {
+        this(null, locale, suggests, flags, null);
+    }
+
+    /**
+     * @param context Context for the application
+     * @param locale locale Locale of the suggestions
+     * @param suggests suggests Suggests for the string under the span
+     * @param flags Additional flags indicating how this span is handled in TextView
+     * @param originalString originalString for suggests
+     */
+    public CorrectionSpan(Context context, Locale locale, List<CharSequence> suggests, int flags,
+            String originalString) {
+        final int N = Math.min(SUGGESTS_MAX_SIZE, suggests.size());
+        for (int i = 0; i < N; ++i) {
+            mSuggests.add(suggests.get(i));
+        }
+        mFlags = flags;
+        if (context != null && locale == null) {
+            mLocaleString = context.getResources().getConfiguration().locale.toString();
+        } else {
+            mLocaleString = locale.toString();
+        }
+        mOriginalString = originalString;
+    }
+
+    public CorrectionSpan(Parcel src) {
+        src.readList(mSuggests, null);
+        mFlags = src.readInt();
+        mLocaleString = src.readString();
+        mOriginalString = src.readString();
+    }
+
+    /**
+     * @return suggestions
+     */
+    public List<CharSequence> getSuggests() {
+        return new ArrayList<CharSequence>(mSuggests);
+    }
+
+    /**
+     * @return locale of suggestions
+     */
+    public String getLocale() {
+        return mLocaleString;
+    }
+
+    /**
+     * @return original string of suggestions
+     */
+    public String getOriginalString() {
+        return mOriginalString;
+    }
+
+    public int getFlags() {
+        return mFlags;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeList(mSuggests);
+        dest.writeInt(mFlags);
+        dest.writeString(mLocaleString);
+        dest.writeString(mOriginalString);
+    }
+
+    @Override
+    public int getSpanTypeId() {
+        return TextUtils.CORRECTION_SPAN;
+    }
+
+    public static final Parcelable.Creator<CorrectionSpan> CREATOR =
+            new Parcelable.Creator<CorrectionSpan>() {
+        @Override
+        public CorrectionSpan createFromParcel(Parcel source) {
+            return new CorrectionSpan(source);
+        }
+
+        @Override
+        public CorrectionSpan[] newArray(int size) {
+            return new CorrectionSpan[size];
+        }
+    };
+}
diff --git a/core/java/android/view/inputmethod/BaseInputConnection.java b/core/java/android/view/inputmethod/BaseInputConnection.java
index e644045..b95e7c9 100644
--- a/core/java/android/view/inputmethod/BaseInputConnection.java
+++ b/core/java/android/view/inputmethod/BaseInputConnection.java
@@ -20,6 +20,7 @@
 import android.content.res.TypedArray;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.SystemClock;
 import android.text.Editable;
 import android.text.NoCopySpan;
@@ -29,6 +30,7 @@
 import android.text.Spanned;
 import android.text.TextUtils;
 import android.text.method.MetaKeyKeyListener;
+import android.text.style.CorrectionSpan;
 import android.util.Log;
 import android.util.LogPrinter;
 import android.view.KeyCharacterMap;
@@ -190,6 +192,15 @@
     }
 
     /**
+     * Default implementation does nothing and returns false.
+     */
+    @Override
+    public boolean setCorrectionSpan(IBinder token, CorrectionSpan correctionSpan, int start,
+            int end, int flags) {
+        return false;
+    }
+
+    /**
      * The default implementation performs the deletion around the current
      * selection position of the editable text.
      */
diff --git a/core/java/android/view/inputmethod/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java
index ea9e402..a8a5346 100644
--- a/core/java/android/view/inputmethod/InputConnection.java
+++ b/core/java/android/view/inputmethod/InputConnection.java
@@ -17,6 +17,8 @@
 package android.view.inputmethod;
 
 import android.os.Bundle;
+import android.os.IBinder;
+import android.text.style.CorrectionSpan;
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
 
@@ -353,4 +355,10 @@
      * valid.
      */
     public boolean performPrivateCommand(String action, Bundle data);
+
+    /**
+     * Add a correction span.
+     */
+    public boolean setCorrectionSpan(IBinder token, CorrectionSpan correctionSpan, int start,
+            int end, int flags);
 }
diff --git a/core/java/android/view/inputmethod/InputConnectionWrapper.java b/core/java/android/view/inputmethod/InputConnectionWrapper.java
index 4d9d51e..fee88d97 100644
--- a/core/java/android/view/inputmethod/InputConnectionWrapper.java
+++ b/core/java/android/view/inputmethod/InputConnectionWrapper.java
@@ -17,6 +17,8 @@
 package android.view.inputmethod;
 
 import android.os.Bundle;
+import android.os.IBinder;
+import android.text.style.CorrectionSpan;
 import android.view.KeyEvent;
 
 /**
@@ -126,4 +128,9 @@
     public boolean performPrivateCommand(String action, Bundle data) {
         return mTarget.performPrivateCommand(action, data);
     }
+
+    public boolean setCorrectionSpan(IBinder token, CorrectionSpan correctionSpan, int start,
+            int end, int flags) {
+        return mTarget.setCorrectionSpan(token, correctionSpan, start, end, flags);
+    }
 }
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index a3ea6a9..82022fb 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -43,6 +43,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.Message;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -81,6 +82,7 @@
 import android.text.method.TimeKeyListener;
 import android.text.method.TransformationMethod;
 import android.text.style.ClickableSpan;
+import android.text.style.CorrectionSpan;
 import android.text.style.ParagraphStyle;
 import android.text.style.URLSpan;
 import android.text.style.UpdateAppearance;
@@ -126,6 +128,7 @@
 import java.io.IOException;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
+import java.util.HashSet;
 
 /**
  * Displays text to the user and optionally allows them to edit it.  A TextView
@@ -8339,6 +8342,19 @@
         sLastCutOrCopyTime = SystemClock.uptimeMillis();
     }
 
+    public boolean setCorrectionSpan(IBinder token, CorrectionSpan span, int start, int end,
+            int flags) {
+        if (getWindowToken() != token || !(mText instanceof Spannable)) return false;
+        Spannable spannable = (Spannable)mText;
+        CorrectionSpan[] spans = spannable.getSpans(start, end, CorrectionSpan.class);
+        final int N = spans.length;
+        for (int i = 0; i < N; ++i) {
+            spannable.removeSpan(spans[i]);
+        }
+        spannable.setSpan(span, start, end, flags);
+        return true;
+    }
+
     /**
      * An ActionMode Callback class that is used to provide actions while in text selection mode.
      *
diff --git a/core/java/com/android/internal/view/IInputConnectionWrapper.java b/core/java/com/android/internal/view/IInputConnectionWrapper.java
index b5df812..8719fde 100644
--- a/core/java/com/android/internal/view/IInputConnectionWrapper.java
+++ b/core/java/com/android/internal/view/IInputConnectionWrapper.java
@@ -18,9 +18,11 @@
 
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
 import android.os.RemoteException;
+import android.text.style.CorrectionSpan;
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.inputmethod.CompletionInfo;
@@ -45,6 +47,7 @@
     private static final int DO_PERFORM_EDITOR_ACTION = 58;
     private static final int DO_PERFORM_CONTEXT_MENU_ACTION = 59;
     private static final int DO_SET_COMPOSING_TEXT = 60;
+    private static final int DO_SET_SECURE_CORRECTION_SPAN = 61;
     private static final int DO_SET_COMPOSING_REGION = 63;
     private static final int DO_FINISH_COMPOSING_TEXT = 65;
     private static final int DO_SEND_KEY_EVENT = 70;
@@ -174,7 +177,14 @@
     public void performPrivateCommand(String action, Bundle data) {
         dispatchMessage(obtainMessageOO(DO_PERFORM_PRIVATE_COMMAND, action, data));
     }
-    
+
+    @Override
+    public void setCorrectionSpan(IBinder token, CorrectionSpan correctionSpan, int start,
+            int end, int flags) {
+        dispatchMessage(obtainMessageOOIII(DO_SET_SECURE_CORRECTION_SPAN, token, correctionSpan,
+                start, end, flags));
+    }
+
     void dispatchMessage(Message msg) {
         // If we are calling this from the main thread, then we can call
         // right through.  Otherwise, we need to send the message to the
@@ -420,6 +430,17 @@
                         (Bundle)args.arg2);
                 return;
             }
+            case DO_SET_SECURE_CORRECTION_SPAN: {
+                InputConnection ic = mInputConnection.get();
+                if (ic == null || !isActive()) {
+                    Log.w(TAG, "setCorrectionSpan on inactive InputConnection");
+                    return;
+                }
+                SomeArgs args = (SomeArgs)msg.obj;
+                ic.setCorrectionSpan((IBinder)args.arg1, (CorrectionSpan)args.arg2, msg.arg1,
+                        msg.arg2, args.seq);
+                return;
+            }
         }
         Log.w(TAG, "Unhandled message code: " + msg.what);
     }
@@ -469,4 +490,12 @@
         args.arg2 = arg2;
         return mH.obtainMessage(what, 0, 0, args);
     }
+
+    Message obtainMessageOOIII(int what, Object arg1, Object arg2, int arg3, int arg4, int arg5) {
+        SomeArgs args = new SomeArgs();
+        args.arg1 = arg1;
+        args.arg2 = arg2;
+        args.seq = arg5;
+        return mH.obtainMessage(what, arg3, arg4, args);
+    }
 }
diff --git a/core/java/com/android/internal/view/IInputContext.aidl b/core/java/com/android/internal/view/IInputContext.aidl
index e00dd4e..eb20d61 100644
--- a/core/java/com/android/internal/view/IInputContext.aidl
+++ b/core/java/com/android/internal/view/IInputContext.aidl
@@ -17,6 +17,7 @@
 package com.android.internal.view;
 
 import android.os.Bundle;
+import android.text.style.CorrectionSpan;
 import android.view.KeyEvent;
 import android.view.inputmethod.CompletionInfo;
 import android.view.inputmethod.CorrectionInfo;
@@ -72,4 +73,7 @@
     void setComposingRegion(int start, int end);
 
     void getSelectedText(int flags, int seq, IInputContextCallback callback);
+
+    void setCorrectionSpan(in IBinder token, in CorrectionSpan correctionSpan, int start,
+            int end, int flags);
 }
diff --git a/core/java/com/android/internal/view/InputConnectionWrapper.java b/core/java/com/android/internal/view/InputConnectionWrapper.java
index b13118a..efe315f 100644
--- a/core/java/com/android/internal/view/InputConnectionWrapper.java
+++ b/core/java/com/android/internal/view/InputConnectionWrapper.java
@@ -17,8 +17,10 @@
 package com.android.internal.view;
 
 import android.os.Bundle;
+import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.text.style.CorrectionSpan;
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.inputmethod.CompletionInfo;
@@ -413,4 +415,14 @@
             return false;
         }
     }
+    @Override
+    public boolean setCorrectionSpan(IBinder token, CorrectionSpan correctionSpan, int start,
+            int end, int flags) {
+        try {
+            mIInputContext.setCorrectionSpan(token, correctionSpan, start, end, flags);
+            return true;
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
 }
diff --git a/core/java/com/android/internal/widget/EditableInputConnection.java b/core/java/com/android/internal/widget/EditableInputConnection.java
index 9f9f020..ea82bc7 100644
--- a/core/java/com/android/internal/widget/EditableInputConnection.java
+++ b/core/java/com/android/internal/widget/EditableInputConnection.java
@@ -17,8 +17,10 @@
 package com.android.internal.widget;
 
 import android.os.Bundle;
+import android.os.IBinder;
 import android.text.Editable;
 import android.text.method.KeyListener;
+import android.text.style.CorrectionSpan;
 import android.util.Log;
 import android.view.inputmethod.BaseInputConnection;
 import android.view.inputmethod.CompletionInfo;
@@ -144,4 +146,13 @@
 
         return success;
     }
+
+    @Override
+    public boolean setCorrectionSpan(IBinder token, CorrectionSpan correctionSpan, int start,
+            int end, int flags) {
+        mTextView.beginBatchEdit();
+        boolean retval = mTextView.setCorrectionSpan(token, correctionSpan, start, end, flags);
+        mTextView.endBatchEdit();
+        return retval;
+    }
 }