diff --git a/api/current.txt b/api/current.txt index 629d832c0c3cd..c2e6ff3140c58 100644 --- a/api/current.txt +++ b/api/current.txt @@ -43334,6 +43334,7 @@ package android.view.inputmethod { method public boolean commitCorrection(android.view.inputmethod.CorrectionInfo); method public boolean commitText(java.lang.CharSequence, int); method public boolean deleteSurroundingText(int, int); + method public boolean deleteSurroundingTextInCodePoints(int, int); method public boolean endBatchEdit(); method public boolean finishComposingText(); method public static int getComposingSpanEnd(android.text.Spannable); @@ -43499,6 +43500,7 @@ package android.view.inputmethod { method public abstract boolean commitCorrection(android.view.inputmethod.CorrectionInfo); method public abstract boolean commitText(java.lang.CharSequence, int); method public abstract boolean deleteSurroundingText(int, int); + method public abstract boolean deleteSurroundingTextInCodePoints(int, int); method public abstract boolean endBatchEdit(); method public abstract boolean finishComposingText(); method public abstract int getCursorCapsMode(int); @@ -43529,6 +43531,7 @@ package android.view.inputmethod { method public boolean commitCorrection(android.view.inputmethod.CorrectionInfo); method public boolean commitText(java.lang.CharSequence, int); method public boolean deleteSurroundingText(int, int); + method public boolean deleteSurroundingTextInCodePoints(int, int); method public boolean endBatchEdit(); method public boolean finishComposingText(); method public int getCursorCapsMode(int); diff --git a/api/system-current.txt b/api/system-current.txt index 6a36ce05acca2..3ec14c8a4b371 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -45688,6 +45688,7 @@ package android.view.inputmethod { method public boolean commitCorrection(android.view.inputmethod.CorrectionInfo); method public boolean commitText(java.lang.CharSequence, int); method public boolean deleteSurroundingText(int, int); + method public boolean deleteSurroundingTextInCodePoints(int, int); method public boolean endBatchEdit(); method public boolean finishComposingText(); method public static int getComposingSpanEnd(android.text.Spannable); @@ -45853,6 +45854,7 @@ package android.view.inputmethod { method public abstract boolean commitCorrection(android.view.inputmethod.CorrectionInfo); method public abstract boolean commitText(java.lang.CharSequence, int); method public abstract boolean deleteSurroundingText(int, int); + method public abstract boolean deleteSurroundingTextInCodePoints(int, int); method public abstract boolean endBatchEdit(); method public abstract boolean finishComposingText(); method public abstract int getCursorCapsMode(int); @@ -45883,6 +45885,7 @@ package android.view.inputmethod { method public boolean commitCorrection(android.view.inputmethod.CorrectionInfo); method public boolean commitText(java.lang.CharSequence, int); method public boolean deleteSurroundingText(int, int); + method public boolean deleteSurroundingTextInCodePoints(int, int); method public boolean endBatchEdit(); method public boolean finishComposingText(); method public int getCursorCapsMode(int); diff --git a/api/test-current.txt b/api/test-current.txt index 21b101fef4499..bc18278233b91 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -43350,6 +43350,7 @@ package android.view.inputmethod { method public boolean commitCorrection(android.view.inputmethod.CorrectionInfo); method public boolean commitText(java.lang.CharSequence, int); method public boolean deleteSurroundingText(int, int); + method public boolean deleteSurroundingTextInCodePoints(int, int); method public boolean endBatchEdit(); method public boolean finishComposingText(); method public static int getComposingSpanEnd(android.text.Spannable); @@ -43515,6 +43516,7 @@ package android.view.inputmethod { method public abstract boolean commitCorrection(android.view.inputmethod.CorrectionInfo); method public abstract boolean commitText(java.lang.CharSequence, int); method public abstract boolean deleteSurroundingText(int, int); + method public abstract boolean deleteSurroundingTextInCodePoints(int, int); method public abstract boolean endBatchEdit(); method public abstract boolean finishComposingText(); method public abstract int getCursorCapsMode(int); @@ -43545,6 +43547,7 @@ package android.view.inputmethod { method public boolean commitCorrection(android.view.inputmethod.CorrectionInfo); method public boolean commitText(java.lang.CharSequence, int); method public boolean deleteSurroundingText(int, int); + method public boolean deleteSurroundingTextInCodePoints(int, int); method public boolean endBatchEdit(); method public boolean finishComposingText(); method public int getCursorCapsMode(int); diff --git a/core/java/android/view/inputmethod/BaseInputConnection.java b/core/java/android/view/inputmethod/BaseInputConnection.java index 6e5e5915bb6f8..5919f9e896698 100644 --- a/core/java/android/view/inputmethod/BaseInputConnection.java +++ b/core/java/android/view/inputmethod/BaseInputConnection.java @@ -230,7 +230,7 @@ public boolean deleteSurroundingText(int beforeLength, int afterLength) { b = tmp; } - // ignore the composing text. + // Ignore the composing text. int ca = getComposingSpanStart(content); int cb = getComposingSpanEnd(content); if (cb < ca) { @@ -266,6 +266,167 @@ public boolean deleteSurroundingText(int beforeLength, int afterLength) { return true; } + private static int INVALID_INDEX = -1; + private static int findIndexBackward(final CharSequence cs, final int from, + final int numCodePoints) { + int currentIndex = from; + boolean waitingHighSurrogate = false; + final int N = cs.length(); + if (currentIndex < 0 || N < currentIndex) { + return INVALID_INDEX; // The starting point is out of range. + } + if (numCodePoints < 0) { + return INVALID_INDEX; // Basically this should not happen. + } + int remainingCodePoints = numCodePoints; + while (true) { + if (remainingCodePoints == 0) { + return currentIndex; // Reached to the requested length in code points. + } + + --currentIndex; + if (currentIndex < 0) { + if (waitingHighSurrogate) { + return INVALID_INDEX; // An invalid surrogate pair is found. + } + return 0; // Reached to the beginning of the text w/o any invalid surrogate pair. + } + final char c = cs.charAt(currentIndex); + if (waitingHighSurrogate) { + if (!java.lang.Character.isHighSurrogate(c)) { + return INVALID_INDEX; // An invalid surrogate pair is found. + } + waitingHighSurrogate = false; + --remainingCodePoints; + continue; + } + if (!java.lang.Character.isSurrogate(c)) { + --remainingCodePoints; + continue; + } + if (java.lang.Character.isHighSurrogate(c)) { + return INVALID_INDEX; // A invalid surrogate pair is found. + } + waitingHighSurrogate = true; + } + } + + private static int findIndexForward(final CharSequence cs, final int from, + final int numCodePoints) { + int currentIndex = from; + boolean waitingLowSurrogate = false; + final int N = cs.length(); + if (currentIndex < 0 || N < currentIndex) { + return INVALID_INDEX; // The starting point is out of range. + } + if (numCodePoints < 0) { + return INVALID_INDEX; // Basically this should not happen. + } + int remainingCodePoints = numCodePoints; + + while (true) { + if (remainingCodePoints == 0) { + return currentIndex; // Reached to the requested length in code points. + } + + if (currentIndex >= N) { + if (waitingLowSurrogate) { + return INVALID_INDEX; // An invalid surrogate pair is found. + } + return N; // Reached to the end of the text w/o any invalid surrogate pair. + } + final char c = cs.charAt(currentIndex); + if (waitingLowSurrogate) { + if (!java.lang.Character.isLowSurrogate(c)) { + return INVALID_INDEX; // An invalid surrogate pair is found. + } + --remainingCodePoints; + waitingLowSurrogate = false; + ++currentIndex; + continue; + } + if (!java.lang.Character.isSurrogate(c)) { + --remainingCodePoints; + ++currentIndex; + continue; + } + if (java.lang.Character.isLowSurrogate(c)) { + return INVALID_INDEX; // A invalid surrogate pair is found. + } + waitingLowSurrogate = true; + ++currentIndex; + } + } + + /** + * The default implementation performs the deletion around the current selection position of the + * editable text. + * @param beforeLength The number of characters before the cursor to be deleted, in code points. + * If this is greater than the number of existing characters between the beginning of the + * text and the cursor, then this method does not fail but deletes all the characters in + * that range. + * @param afterLength The number of characters after the cursor to be deleted, in code points. + * If this is greater than the number of existing characters between the cursor and + * the end of the text, then this method does not fail but deletes all the characters in + * that range. + */ + public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) { + if (DEBUG) Log.v(TAG, "deleteSurroundingText " + beforeLength + + " / " + afterLength); + final Editable content = getEditable(); + if (content == null) return false; + + beginBatchEdit(); + + int a = Selection.getSelectionStart(content); + int b = Selection.getSelectionEnd(content); + + if (a > b) { + int tmp = a; + a = b; + b = tmp; + } + + // Ignore the composing text. + int ca = getComposingSpanStart(content); + int cb = getComposingSpanEnd(content); + if (cb < ca) { + int tmp = ca; + ca = cb; + cb = tmp; + } + if (ca != -1 && cb != -1) { + if (ca < a) a = ca; + if (cb > b) b = cb; + } + + if (a >= 0 && b >= 0) { + final int start = findIndexBackward(content, a, Math.max(beforeLength, 0)); + if (start != INVALID_INDEX) { + final int end = findIndexForward(content, b, Math.max(afterLength, 0)); + if (end != INVALID_INDEX) { + final int numDeleteBefore = a - start; + if (numDeleteBefore > 0) { + content.delete(start, a); + } + final int numDeleteAfter = end - b; + if (numDeleteAfter > 0) { + content.delete(b - numDeleteBefore, end - numDeleteBefore); + } + } + } + // NOTE: You may think we should return false here if start and/or end is INVALID_INDEX, + // but the truth is that IInputConnectionWrapper running in the middle of IPC calls + // always returns true to the IME without waiting for the completion of this method as + // IInputConnectionWrapper#isAtive() returns true. This is actually why some methods + // including this method look like asynchronous calls from the IME. + } + + endBatchEdit(); + + return true; + } + /** * The default implementation removes the composing state from the * current editable text. In addition, only if dummy mode, a key event is diff --git a/core/java/android/view/inputmethod/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java index be7bc14f05157..eb773e23fa623 100644 --- a/core/java/android/view/inputmethod/InputConnection.java +++ b/core/java/android/view/inputmethod/InputConnection.java @@ -347,6 +347,33 @@ public ExtractedText getExtractedText(ExtractedTextRequest request, */ public boolean deleteSurroundingText(int beforeLength, int afterLength); + /** + * A variant of {@link #deleteSurroundingText(int, int)}. Major differences are: + * + * + * + *

Editor authors: In addition to the requirement in + * {@link #deleteSurroundingText(int, int)}, make sure to do nothing when one ore more invalid + * surrogate pairs are found in the requested range.

+ * + * @see #deleteSurroundingText(int, int) + * + * @param beforeLength The number of characters before the cursor to be deleted, in code points. + * If this is greater than the number of existing characters between the beginning of the + * text and the cursor, then this method does not fail but deletes all the characters in + * that range. + * @param afterLength The number of characters after the cursor to be deleted, in code points. + * If this is greater than the number of existing characters between the cursor and + * the end of the text, then this method does not fail but deletes all the characters in + * that range. + * @return true on success, false if the input connection is no longer valid. + */ + public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength); + /** * Replace the currently composing text with the given text, and * set the new cursor position. Any composing text set previously diff --git a/core/java/android/view/inputmethod/InputConnectionWrapper.java b/core/java/android/view/inputmethod/InputConnectionWrapper.java index 231aa070672ad..e5ae42299dbac 100644 --- a/core/java/android/view/inputmethod/InputConnectionWrapper.java +++ b/core/java/android/view/inputmethod/InputConnectionWrapper.java @@ -62,6 +62,10 @@ public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { return mTarget.getExtractedText(request, flags); } + public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) { + return mTarget.deleteSurroundingTextInCodePoints(beforeLength, afterLength); + } + public boolean deleteSurroundingText(int beforeLength, int afterLength) { return mTarget.deleteSurroundingText(beforeLength, afterLength); } diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index e1ce9fe7c0390..6241a4cd34622 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -5802,6 +5802,11 @@ public boolean deleteSurroundingText(int beforeLength, int afterLength) { return getTarget().deleteSurroundingText(beforeLength, afterLength); } + @Override + public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) { + return getTarget().deleteSurroundingTextInCodePoints(beforeLength, afterLength); + } + @Override public boolean setComposingText(CharSequence text, int newCursorPosition) { return getTarget().setComposingText(text, newCursorPosition); diff --git a/core/java/com/android/internal/view/IInputConnectionWrapper.java b/core/java/com/android/internal/view/IInputConnectionWrapper.java index 85ec29c4634ca..b77cc6365d8a1 100644 --- a/core/java/com/android/internal/view/IInputConnectionWrapper.java +++ b/core/java/com/android/internal/view/IInputConnectionWrapper.java @@ -49,6 +49,7 @@ public class IInputConnectionWrapper extends IInputContext.Stub { private static final int DO_FINISH_COMPOSING_TEXT = 65; private static final int DO_SEND_KEY_EVENT = 70; private static final int DO_DELETE_SURROUNDING_TEXT = 80; + private static final int DO_DELETE_SURROUNDING_TEXT_IN_CODE_POINTS = 81; private static final int DO_BEGIN_BATCH_EDIT = 90; private static final int DO_END_BATCH_EDIT = 95; private static final int DO_REPORT_FULLSCREEN_MODE = 100; @@ -155,9 +156,14 @@ public void clearMetaKeyStates(int states) { dispatchMessage(obtainMessageII(DO_CLEAR_META_KEY_STATES, states, 0)); } - public void deleteSurroundingText(int leftLength, int rightLength) { + public void deleteSurroundingText(int beforeLength, int afterLength) { dispatchMessage(obtainMessageII(DO_DELETE_SURROUNDING_TEXT, - leftLength, rightLength)); + beforeLength, afterLength)); + } + + public void deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) { + dispatchMessage(obtainMessageII(DO_DELETE_SURROUNDING_TEXT_IN_CODE_POINTS, + beforeLength, afterLength)); } public void beginBatchEdit() { @@ -389,6 +395,15 @@ void executeMessage(Message msg) { ic.deleteSurroundingText(msg.arg1, msg.arg2); return; } + case DO_DELETE_SURROUNDING_TEXT_IN_CODE_POINTS: { + InputConnection ic = mInputConnection.get(); + if (ic == null || !isActive()) { + Log.w(TAG, "deleteSurroundingTextInCodePoints on inactive InputConnection"); + return; + } + ic.deleteSurroundingTextInCodePoints(msg.arg1, msg.arg2); + return; + } case DO_BEGIN_BATCH_EDIT: { InputConnection ic = mInputConnection.get(); if (ic == null || !isActive()) { @@ -454,7 +469,7 @@ Message obtainMessage(int what) { Message obtainMessageII(int what, int arg1, int arg2) { return mH.obtainMessage(what, arg1, arg2); } - + Message obtainMessageO(int what, Object arg1) { return mH.obtainMessage(what, 0, 0, arg1); } diff --git a/core/java/com/android/internal/view/IInputContext.aidl b/core/java/com/android/internal/view/IInputContext.aidl index fd2b513fcb1ab..f7ec420ba6769 100644 --- a/core/java/com/android/internal/view/IInputContext.aidl +++ b/core/java/com/android/internal/view/IInputContext.aidl @@ -38,8 +38,9 @@ import com.android.internal.view.IInputContextCallback; void getExtractedText(in ExtractedTextRequest request, int flags, int seq, IInputContextCallback callback); - - void deleteSurroundingText(int leftLength, int rightLength); + + void deleteSurroundingText(int beforeLength, int afterLength); + void deleteSurroundingTextInCodePoints(int beforeLength, int afterLength); void setComposingText(CharSequence text, int newCursorPosition); diff --git a/core/java/com/android/internal/view/InputConnectionWrapper.java b/core/java/com/android/internal/view/InputConnectionWrapper.java index 7dc927f744f6f..94790c1585597 100644 --- a/core/java/com/android/internal/view/InputConnectionWrapper.java +++ b/core/java/com/android/internal/view/InputConnectionWrapper.java @@ -400,7 +400,7 @@ public boolean clearMetaKeyStates(int states) { return false; } } - + public boolean deleteSurroundingText(int beforeLength, int afterLength) { try { mIInputContext.deleteSurroundingText(beforeLength, afterLength); @@ -410,6 +410,15 @@ public boolean deleteSurroundingText(int beforeLength, int afterLength) { } } + public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) { + try { + mIInputContext.deleteSurroundingTextInCodePoints(beforeLength, afterLength); + return true; + } catch (RemoteException e) { + return false; + } + } + public boolean reportFullscreenMode(boolean enabled) { try { mIInputContext.reportFullscreenMode(enabled);