diff --git a/core/java/android/text/Emoji.java b/core/java/android/text/Emoji.java index b6d720d453e9e..83810b03b4633 100644 --- a/core/java/android/text/Emoji.java +++ b/core/java/android/text/Emoji.java @@ -164,6 +164,8 @@ public class Emoji { public static int VARIATION_SELECTOR_16 = 0xFE0F; + public static int CANCEL_TAG = 0xE007F; + // Returns true if the given code point is regional indicator symbol. public static boolean isRegionalIndicatorSymbol(int codepoint) { return 0x1F1E6 <= codepoint && codepoint <= 0x1F1FF; @@ -188,4 +190,13 @@ public class Emoji { public static boolean isKeycapBase(int codePoint) { return ('0' <= codePoint && codePoint <= '9') || codePoint == '#' || codePoint == '*'; } + + /** + * Returns true if the character can be a part of tag_spec in emoji tag sequence. + * + * Note that 0xE007F (CANCEL TAG) is not included. + */ + public static boolean isTagSpecChar(int codePoint) { + return 0xE0020 <= codePoint && codePoint <= 0xE007E; + } } diff --git a/core/java/android/text/method/BaseKeyListener.java b/core/java/android/text/method/BaseKeyListener.java index 90559dcdbcf4e..5f0a46d815ea6 100644 --- a/core/java/android/text/method/BaseKeyListener.java +++ b/core/java/android/text/method/BaseKeyListener.java @@ -145,8 +145,11 @@ public abstract class BaseKeyListener extends MetaKeyKeyListener // The number of following RIS code points is even. final int STATE_EVEN_NUMBERED_RIS = 11; + // The offset is in emoji tag sequence. + final int STATE_IN_TAG_SEQUENCE = 12; + // The state machine has been stopped. - final int STATE_FINISHED = 12; + final int STATE_FINISHED = 13; int deleteCharCount = 0; // Char count to be deleted by backspace. int lastSeenVSCharCount = 0; // Char count of previous variation selector. @@ -173,6 +176,8 @@ public abstract class BaseKeyListener extends MetaKeyKeyListener state = STATE_BEFORE_KEYCAP; } else if (Emoji.isEmoji(codePoint)) { state = STATE_BEFORE_EMOJI; + } else if (codePoint == Emoji.CANCEL_TAG) { + state = STATE_IN_TAG_SEQUENCE; } else { state = STATE_FINISHED; } @@ -275,6 +280,20 @@ public abstract class BaseKeyListener extends MetaKeyKeyListener state = STATE_FINISHED; } break; + case STATE_IN_TAG_SEQUENCE: + if (Emoji.isTagSpecChar(codePoint)) { + deleteCharCount += 2; /* Char count of emoji tag spec character. */ + // Keep the same state. + } else if (Emoji.isEmoji(codePoint)) { + deleteCharCount += Character.charCount(codePoint); + state = STATE_FINISHED; + } else { + // Couldn't find tag_base character. Delete the last tag_term character. + deleteCharCount = 2; // for U+E007F + state = STATE_FINISHED; + } + // TODO: Need handle emoji variation selectors. Issue 35224297 + break; default: throw new IllegalArgumentException("state " + state + " is unknown"); } diff --git a/core/tests/coretests/src/android/text/method/BackspaceTest.java b/core/tests/coretests/src/android/text/method/BackspaceTest.java index a260e947958e8..864b48a79bb09 100644 --- a/core/tests/coretests/src/android/text/method/BackspaceTest.java +++ b/core/tests/coretests/src/android/text/method/BackspaceTest.java @@ -37,23 +37,12 @@ public class BackspaceTest extends KeyListenerTestCase { // Sync the state to the TextView and call onKeyDown with KEYCODE_DEL key event. // Then update the state to the result of TextView. private void backspace(final EditorState state, int modifiers) { - mActivity.runOnUiThread(new Runnable() { - public void run() { - mTextView.setText(state.mText, BufferType.EDITABLE); - mTextView.setKeyListener(mKeyListener); - mTextView.setSelection(state.mSelectionStart, state.mSelectionEnd); - } - }); - mInstrumentation.waitForIdleSync(); - assertTrue(mTextView.hasWindowFocus()); + mTextView.setText(state.mText, BufferType.EDITABLE); + mTextView.setKeyListener(mKeyListener); + mTextView.setSelection(state.mSelectionStart, state.mSelectionEnd); final KeyEvent keyEvent = getKey(KeyEvent.KEYCODE_DEL, modifiers); - mActivity.runOnUiThread(new Runnable() { - public void run() { - mTextView.onKeyDown(keyEvent.getKeyCode(), keyEvent); - } - }); - mInstrumentation.waitForIdleSync(); + mTextView.onKeyDown(keyEvent.getKeyCode(), keyEvent); state.mText = mTextView.getText(); state.mSelectionStart = mTextView.getSelectionStart(); @@ -247,6 +236,51 @@ public class BackspaceTest extends KeyListenerTestCase { state.assertEquals("U+1F1FA U+1F1F8 |"); backspace(state, 0); state.assertEquals("|"); + + // Incomplete sequence. (no tag_term: U+E007E) + state.setByString("'a' U+1F3F4 U+E0067 'b' |"); + backspace(state, 0); + state.assertEquals("'a' U+1F3F4 U+E0067 |"); + backspace(state, 0); + state.assertEquals("'a' U+1F3F4 |"); + backspace(state, 0); + state.assertEquals("'a' |"); + + // No tag_base + state.setByString("'a' U+E0067 U+E007F 'b' |"); + backspace(state, 0); + state.assertEquals("'a' U+E0067 U+E007F |"); + backspace(state, 0); + state.assertEquals("'a' U+E0067 |"); + backspace(state, 0); + state.assertEquals("'a' |"); + + // Isolated tag chars + state.setByString("'a' U+E0067 U+E0067 'b' |"); + backspace(state, 0); + state.assertEquals("'a' U+E0067 U+E0067 |"); + backspace(state, 0); + state.assertEquals("'a' U+E0067 |"); + backspace(state, 0); + state.assertEquals("'a' |"); + + // Isolated tab term. + state.setByString("'a' U+E007F U+E007F 'b' |"); + backspace(state, 0); + state.assertEquals("'a' U+E007F U+E007F |"); + backspace(state, 0); + state.assertEquals("'a' U+E007F |"); + backspace(state, 0); + state.assertEquals("'a' |"); + + // Immediate tag_term after tag_base + state.setByString("'a' U+1F3F4 U+E007F U+1F3F4 U+E007F 'b' |"); + backspace(state, 0); + state.assertEquals("'a' U+1F3F4 U+E007F U+1F3F4 U+E007F |"); + backspace(state, 0); + state.assertEquals("'a' U+1F3F4 U+E007F |"); + backspace(state, 0); + state.assertEquals("'a' |"); } @SmallTest diff --git a/core/tests/coretests/src/android/text/method/ForwardDeleteTest.java b/core/tests/coretests/src/android/text/method/ForwardDeleteTest.java index 1990fd021203a..839d380f79b9a 100644 --- a/core/tests/coretests/src/android/text/method/ForwardDeleteTest.java +++ b/core/tests/coretests/src/android/text/method/ForwardDeleteTest.java @@ -37,23 +37,12 @@ public class ForwardDeleteTest extends KeyListenerTestCase { // Sync the state to the TextView and call onKeyDown with KEYCODE_FORWARD_DEL key event. // Then update the state to the result of TextView. private void forwardDelete(final EditorState state, int modifiers) { - mActivity.runOnUiThread(new Runnable() { - public void run() { - mTextView.setText(state.mText, BufferType.EDITABLE); - mTextView.setKeyListener(mKeyListener); - mTextView.setSelection(state.mSelectionStart, state.mSelectionEnd); - } - }); - mInstrumentation.waitForIdleSync(); - assertTrue(mTextView.hasWindowFocus()); + mTextView.setText(state.mText, BufferType.EDITABLE); + mTextView.setKeyListener(mKeyListener); + mTextView.setSelection(state.mSelectionStart, state.mSelectionEnd); final KeyEvent keyEvent = getKey(KeyEvent.KEYCODE_FORWARD_DEL, modifiers); - mActivity.runOnUiThread(new Runnable() { - public void run() { - mTextView.onKeyDown(keyEvent.getKeyCode(), keyEvent); - } - }); - mInstrumentation.waitForIdleSync(); + mTextView.onKeyDown(keyEvent.getKeyCode(), keyEvent); state.mText = mTextView.getText(); state.mSelectionStart = mTextView.getSelectionStart(); @@ -186,6 +175,46 @@ public class ForwardDeleteTest extends KeyListenerTestCase { state.assertEquals("| U+1F1FA"); forwardDelete(state, 0); state.assertEquals("|"); + + // Incomplete sequence. (no tag_term:U+E007E) + state.setByString("| 'a' U+1F3F4 U+E0067 'b'"); + forwardDelete(state, 0); + state.assertEquals("| U+1F3F4 U+E0067 'b'"); + forwardDelete(state, 0); + state.assertEquals("| 'b'"); + + // No tag_base + state.setByString("| 'a' U+E0067 U+E007F 'b'"); + forwardDelete(state, 0); + state.assertEquals("| 'b'"); + + // Isolated tag chars + state.setByString("| 'a' U+E0067 U+E0067 'b'"); + forwardDelete(state, 0); + state.assertEquals("| 'b'"); + + // Isolated tag base. + state.setByString("| 'a' U+1F3F4 U+1F3F4 'b'"); + forwardDelete(state, 0); + state.assertEquals("| U+1F3F4 U+1F3F4 'b'"); + forwardDelete(state, 0); + state.assertEquals("| U+1F3F4 'b'"); + forwardDelete(state, 0); + state.assertEquals("| 'b'"); + + // Isolated tab term. + state.setByString("| 'a' U+E007F U+E007F 'b'"); + forwardDelete(state, 0); + state.assertEquals("| 'b'"); + + // Immediate tag_term after tag_base + state.setByString("| 'a' U+1F3F4 U+E007F U+1F3F4 U+E007F 'b'"); + forwardDelete(state, 0); + state.assertEquals("| U+1F3F4 U+E007F U+1F3F4 U+E007F 'b'"); + forwardDelete(state, 0); + state.assertEquals("| U+1F3F4 U+E007F 'b'"); + forwardDelete(state, 0); + state.assertEquals("| 'b'"); } @SmallTest diff --git a/core/tests/coretests/src/android/text/method/KeyListenerTestCase.java b/core/tests/coretests/src/android/text/method/KeyListenerTestCase.java index 4b4e7afdb636e..f005d7b02a624 100644 --- a/core/tests/coretests/src/android/text/method/KeyListenerTestCase.java +++ b/core/tests/coretests/src/android/text/method/KeyListenerTestCase.java @@ -17,41 +17,26 @@ package android.text.method; import android.app.Instrumentation; -import android.test.ActivityInstrumentationTestCase2; -import android.text.format.DateUtils; +import android.test.InstrumentationTestCase; import android.view.KeyEvent; import android.widget.EditText; -import android.widget.TextViewActivity; import com.android.frameworks.coretests.R; -public abstract class KeyListenerTestCase extends - ActivityInstrumentationTestCase2 { +public abstract class KeyListenerTestCase extends InstrumentationTestCase { - protected TextViewActivity mActivity; protected Instrumentation mInstrumentation; protected EditText mTextView; public KeyListenerTestCase() { - super(TextViewActivity.class); } @Override protected void setUp() throws Exception { super.setUp(); - mActivity = getActivity(); mInstrumentation = getInstrumentation(); - mTextView = (EditText) mActivity.findViewById(R.id.textview); - - mActivity.runOnUiThread(new Runnable() { - public void run() { - // Ensure that the screen is on for this test. - mTextView.setKeepScreenOn(true); - } - }); - - assertTrue(mActivity.waitForWindowFocus(5 * DateUtils.SECOND_IN_MILLIS)); + mTextView = new EditText(mInstrumentation.getContext()); } protected static KeyEvent getKey(int keycode, int metaState) {