Add Apis to send notifications when the suggestion was picked

- Due to a strong request from VoiceIME

Bug: 4443922

Change-Id: Ia539de0acf66053e0349daec459d75e36805f6bf
diff --git a/api/current.txt b/api/current.txt
index 6854965..94e4210 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -19381,17 +19381,22 @@
   public class SuggestionSpan implements android.text.ParcelableSpan {
     ctor public SuggestionSpan(android.content.Context, java.lang.String[], int);
     ctor public SuggestionSpan(java.util.Locale, java.lang.String[], int);
-    ctor public SuggestionSpan(android.content.Context, java.util.Locale, java.lang.String[], int, java.lang.String);
+    ctor public SuggestionSpan(android.content.Context, java.util.Locale, java.lang.String[], int, java.lang.Class<?>);
     ctor public SuggestionSpan(android.os.Parcel);
     method public int describeContents();
     method public int getFlags();
     method public java.lang.String getLocale();
-    method public java.lang.String getOriginalString();
+    method public java.lang.Class<?> getNotificationTargetClass();
     method public int getSpanTypeId();
     method public java.lang.String[] getSuggestions();
     method public void writeToParcel(android.os.Parcel, int);
+    field public static final java.lang.String ACTION_SUGGESTION_PICKED = "android.text.style.SUGGESTION_PICKED";
     field public static final android.os.Parcelable.Creator CREATOR;
     field public static final int FLAG_VERBATIM = 1; // 0x1
+    field public static final int SUGGESTIONS_MAX_SIZE = 5; // 0x5
+    field public static final java.lang.String SUGGESTION_SPAN_PICKED_AFTER = "after";
+    field public static final java.lang.String SUGGESTION_SPAN_PICKED_BEFORE = "before";
+    field public static final java.lang.String SUGGESTION_SPAN_PICKED_HASHCODE = "hashcode";
   }
 
   public class SuperscriptSpan extends android.text.style.MetricAffectingSpan implements android.text.ParcelableSpan {
diff --git a/core/java/android/text/style/SuggestionSpan.java b/core/java/android/text/style/SuggestionSpan.java
index dcb0898..effa5c8 100644
--- a/core/java/android/text/style/SuggestionSpan.java
+++ b/core/java/android/text/style/SuggestionSpan.java
@@ -19,8 +19,10 @@
 import android.content.Context;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.SystemClock;
 import android.text.ParcelableSpan;
 import android.text.TextUtils;
+import android.util.Log;
 
 import java.util.Arrays;
 import java.util.Locale;
@@ -29,6 +31,7 @@
  * Holds suggestion candidates of words under this span.
  */
 public class SuggestionSpan implements ParcelableSpan {
+    private static final String TAG = SuggestionSpan.class.getSimpleName();
 
     /**
      * Flag for indicating that the input is verbatim. TextView refers to this flag to determine
@@ -36,7 +39,12 @@
      */
     public static final int FLAG_VERBATIM = 0x0001;
 
-    private static final int SUGGESTIONS_MAX_SIZE = 5;
+    public static final String ACTION_SUGGESTION_PICKED = "android.text.style.SUGGESTION_PICKED";
+    public static final String SUGGESTION_SPAN_PICKED_AFTER = "after";
+    public static final String SUGGESTION_SPAN_PICKED_BEFORE = "before";
+    public static final String SUGGESTION_SPAN_PICKED_HASHCODE = "hashcode";
+
+    public static final int SUGGESTIONS_MAX_SIZE = 5;
 
     /*
      * TODO: Needs to check the validity and add a feature that TextView will change
@@ -48,7 +56,9 @@
     private final int mFlags;
     private final String[] mSuggestions;
     private final String mLocaleString;
-    private final String mOriginalString;
+    private final Class<?> mNotificationTargetClass;
+    private final int mHashCode;
+
     /*
      * TODO: If switching IME is required, needs to add parameters for ids of InputMethodInfo
      * and InputMethodSubtype.
@@ -77,10 +87,11 @@
      * @param locale locale Locale of the suggestions
      * @param suggestions Suggestions for the string under the span
      * @param flags Additional flags indicating how this span is handled in TextView
-     * @param originalString originalString for suggestions
+     * @param notificationTargetClass if not null, this class will get notified when the user
+     * selects one of the suggestions.
      */
     public SuggestionSpan(Context context, Locale locale, String[] suggestions, int flags,
-            String originalString) {
+            Class<?> notificationTargetClass) {
         final int N = Math.min(SUGGESTIONS_MAX_SIZE, suggestions.length);
         mSuggestions = Arrays.copyOf(suggestions, N);
         mFlags = flags;
@@ -89,14 +100,26 @@
         } else {
             mLocaleString = locale.toString();
         }
-        mOriginalString = originalString;
+        mNotificationTargetClass = notificationTargetClass;
+        mHashCode = hashCodeInternal(
+                mFlags, mSuggestions, mLocaleString, mNotificationTargetClass);
     }
 
     public SuggestionSpan(Parcel src) {
         mSuggestions = src.readStringArray();
         mFlags = src.readInt();
         mLocaleString = src.readString();
-        mOriginalString = src.readString();
+        Class<?> tempClass = null;
+        try {
+            final String className = src.readString();
+            if (!TextUtils.isEmpty(className)) {
+                tempClass = Class.forName(className);
+            }
+        } catch (ClassNotFoundException e) {
+            Log.i(TAG, "Invalid class name was created.");
+        }
+        mNotificationTargetClass = tempClass;
+        mHashCode = src.readInt();
     }
 
     /**
@@ -114,10 +137,13 @@
     }
 
     /**
-     * @return original string of suggestions
+     * @return The class to notify. The class of the original IME package will receive
+     * a notification when the user selects one of the suggestions. The notification will include
+     * the original string, the suggested replacement string as well as the hashCode of this span.
+     * The class will get notified by an intent that has those information.
      */
-    public String getOriginalString() {
-        return mOriginalString;
+    public Class<?> getNotificationTargetClass() {
+        return mNotificationTargetClass;
     }
 
     public int getFlags() {
@@ -134,7 +160,10 @@
         dest.writeStringArray(mSuggestions);
         dest.writeInt(mFlags);
         dest.writeString(mLocaleString);
-        dest.writeString(mOriginalString);
+        dest.writeString(mNotificationTargetClass != null
+                ? mNotificationTargetClass.getCanonicalName()
+                : "");
+        dest.writeInt(mHashCode);
     }
 
     @Override
@@ -142,6 +171,20 @@
         return TextUtils.SUGGESTION_SPAN;
     }
 
+    @Override
+    public int hashCode() {
+        return mHashCode;
+    }
+
+    private static int hashCodeInternal(int flags, String[] suggestions,String locale,
+            Class<?> notificationTargetClass) {
+        final String cls = notificationTargetClass != null
+                ? notificationTargetClass.getCanonicalName()
+                : "";
+        return Arrays.hashCode(
+                new Object[] {SystemClock.uptimeMillis(), flags, suggestions, locale, cls});
+    }
+
     public static final Parcelable.Creator<SuggestionSpan> CREATOR =
             new Parcelable.Creator<SuggestionSpan>() {
         @Override
diff --git a/core/java/android/view/inputmethod/BaseInputConnection.java b/core/java/android/view/inputmethod/BaseInputConnection.java
index b4303f4..abe3c2c 100644
--- a/core/java/android/view/inputmethod/BaseInputConnection.java
+++ b/core/java/android/view/inputmethod/BaseInputConnection.java
@@ -49,8 +49,9 @@
     private static final boolean DEBUG = false;
     private static final String TAG = "BaseInputConnection";
     static final Object COMPOSING = new ComposingText();
-    
-    final InputMethodManager mIMM;
+
+    /** @hide */
+    protected final InputMethodManager mIMM;
     final View mTargetView;
     final boolean mDummyMode;
     
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 27cbaf7..ea66d67 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -35,6 +35,7 @@
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.ServiceManager;
+import android.text.style.SuggestionSpan;
 import android.util.Log;
 import android.util.PrintWriterPrinter;
 import android.util.Printer;
@@ -550,7 +551,25 @@
     public void setFullscreenMode(boolean fullScreen) {
         mFullscreenMode = fullScreen;
     }
-    
+
+    /** @hide */
+    public void registerSuggestionSpansForNotification(SuggestionSpan[] spans) {
+        try {
+            mService.registerSuggestionSpansForNotification(spans);
+        } catch (RemoteException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /** @hide */
+    public void notifySuggestionPicked(SuggestionSpan span, String originalString, int index) {
+        try {
+            mService.notifySuggestionPicked(span, originalString, index);
+        } catch (RemoteException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
     /**
      * Allows you to discover whether the attached input method is running
      * in fullscreen mode.  Return true if it is fullscreen, entirely covering
diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl
index 4ffa4e1..8039fda 100644
--- a/core/java/com/android/internal/view/IInputMethodManager.aidl
+++ b/core/java/com/android/internal/view/IInputMethodManager.aidl
@@ -17,6 +17,7 @@
 package com.android.internal.view;
 
 import android.os.ResultReceiver;
+import android.text.style.SuggestionSpan;
 import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodSubtype;
 import android.view.inputmethod.EditorInfo;
@@ -61,6 +62,8 @@
     void showMySoftInput(in IBinder token, int flags);
     void updateStatusIcon(in IBinder token, String packageName, int iconId);
     void setImeWindowStatus(in IBinder token, int vis, int backDisposition);
+    void registerSuggestionSpansForNotification(in SuggestionSpan[] spans);
+    boolean notifySuggestionPicked(in SuggestionSpan span, String originalString, int index);
     InputMethodSubtype getCurrentInputMethodSubtype();
     boolean setCurrentInputMethodSubtype(in InputMethodSubtype subtype);
     boolean switchToLastInputMethod(in IBinder token);
diff --git a/core/java/com/android/internal/widget/EditableInputConnection.java b/core/java/com/android/internal/widget/EditableInputConnection.java
index 0d32d4ba..32e733b 100644
--- a/core/java/com/android/internal/widget/EditableInputConnection.java
+++ b/core/java/com/android/internal/widget/EditableInputConnection.java
@@ -17,9 +17,10 @@
 package com.android.internal.widget;
 
 import android.os.Bundle;
-import android.os.IBinder;
 import android.text.Editable;
+import android.text.Spanned;
 import android.text.method.KeyListener;
+import android.text.style.SuggestionSpan;
 import android.util.Log;
 import android.view.inputmethod.BaseInputConnection;
 import android.view.inputmethod.CompletionInfo;
@@ -138,6 +139,11 @@
         if (mTextView == null) {
             return super.commitText(text, newCursorPosition);
         }
+        if (text instanceof Spanned) {
+            Spanned spanned = ((Spanned) text);
+            SuggestionSpan[] spans = spanned.getSpans(0, text.length(), SuggestionSpan.class);
+            mIMM.registerSuggestionSpansForNotification(spans);
+        }
 
         mTextView.resetErrorChangedFlag();
         boolean success = super.commitText(text, newCursorPosition);
diff --git a/services/java/com/android/server/InputMethodManagerService.java b/services/java/com/android/server/InputMethodManagerService.java
index c365af5..7361ef7 100644
--- a/services/java/com/android/server/InputMethodManagerService.java
+++ b/services/java/com/android/server/InputMethodManagerService.java
@@ -64,7 +64,9 @@
 import android.provider.Settings.Secure;
 import android.provider.Settings.SettingNotFoundException;
 import android.text.TextUtils;
+import android.text.style.SuggestionSpan;
 import android.util.EventLog;
+import android.util.LruCache;
 import android.util.Pair;
 import android.util.Slog;
 import android.util.PrintWriterPrinter;
@@ -117,6 +119,8 @@
 
     static final long TIME_TO_RECONNECT = 10*1000;
 
+    static final int SECURE_SUGGESTION_SPANS_MAX_SIZE = 20;
+
     private static final int NOT_A_SUBTYPE_ID = -1;
     private static final String NOT_A_SUBTYPE_ID_STR = String.valueOf(NOT_A_SUBTYPE_ID);
     private static final String SUBTYPE_MODE_KEYBOARD = "keyboard";
@@ -141,6 +145,8 @@
     // lock for this class.
     final ArrayList<InputMethodInfo> mMethodList = new ArrayList<InputMethodInfo>();
     final HashMap<String, InputMethodInfo> mMethodMap = new HashMap<String, InputMethodInfo>();
+    private final LruCache<SuggestionSpan, InputMethodInfo> mSecureSuggestionSpans =
+            new LruCache<SuggestionSpan, InputMethodInfo>(SECURE_SUGGESTION_SPANS_MAX_SIZE);
 
     class SessionState {
         final ClientState client;
@@ -965,6 +971,7 @@
         }
     }
 
+    @Override
     public void updateStatusIcon(IBinder token, String packageName, int iconId) {
         int uid = Binder.getCallingUid();
         long ident = Binder.clearCallingIdentity();
@@ -989,6 +996,7 @@
         }
     }
 
+    @Override
     public void setImeWindowStatus(IBinder token, int vis, int backDisposition) {
         int uid = Binder.getCallingUid();
         long ident = Binder.clearCallingIdentity();
@@ -1008,6 +1016,41 @@
         }
     }
 
+    public void registerSuggestionSpansForNotification(SuggestionSpan[] spans) {
+        synchronized (mMethodMap) {
+            final InputMethodInfo currentImi = mMethodMap.get(mCurMethodId);
+            for (int i = 0; i < spans.length; ++i) {
+                SuggestionSpan ss = spans[i];
+                if (ss.getNotificationTargetClass() != null) {
+                    mSecureSuggestionSpans.put(ss, currentImi);
+                }
+            }
+        }
+    }
+
+    public boolean notifySuggestionPicked(SuggestionSpan span, String originalString, int index) {
+        synchronized (mMethodMap) {
+            final InputMethodInfo targetImi = mSecureSuggestionSpans.get(span);
+            // TODO: Do not send the intent if the process of the targetImi is already dead.
+            if (targetImi != null) {
+                final String[] suggestions = span.getSuggestions();
+                if (index < 0 || index >= suggestions.length) return false;
+                final Class<?> c = span.getNotificationTargetClass();
+                final Intent intent = new Intent();
+                // Ensures that only a class in the original IME package will receive the
+                // notification.
+                intent.setClassName(targetImi.getPackageName(), c.getCanonicalName());
+                intent.setAction(SuggestionSpan.ACTION_SUGGESTION_PICKED);
+                intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_BEFORE, originalString);
+                intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_AFTER, suggestions[index]);
+                intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_HASHCODE, span.hashCode());
+                mContext.sendBroadcast(intent);
+                return true;
+            }
+        }
+        return false;
+    }
+
     void updateFromSettingsLocked() {
         // We are assuming that whoever is changing DEFAULT_INPUT_METHOD and
         // ENABLED_INPUT_METHODS is taking care of keeping them correctly in