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