Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 146dd3ad authored by Seigo Nonaka's avatar Seigo Nonaka Committed by android-build-merger
Browse files

Merge "Improve forward delete key handling." into nyc-dev am: 19f47929

am: aa370e1f

* commit 'aa370e1f':
  Improve forward delete key handling.
parents 84f19f00 aa370e1f
Loading
Loading
Loading
Loading
+23 −9
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package android.text.method;

import android.graphics.Paint;
import android.icu.lang.UCharacter;
import android.icu.lang.UProperty;
import android.view.KeyEvent;
@@ -25,6 +26,8 @@ import android.text.method.TextKeyListener.Capitalize;
import android.text.style.ReplacementSpan;
import android.widget.TextView;

import com.android.internal.annotations.GuardedBy;

import java.text.BreakIterator;
import java.util.Arrays;
import java.util.Collections;
@@ -45,6 +48,11 @@ public abstract class BaseKeyListener extends MetaKeyKeyListener
        implements KeyListener {
    /* package */ static final Object OLD_SEL_START = new NoCopySpan.Concrete();

    private final Object mLock = new Object();

    @GuardedBy("mLock")
    static Paint sCachedPaint = null;

    /**
     * Performs the action that happens when you press the {@link KeyEvent#KEYCODE_DEL} key in
     * a {@link TextView}.  If there is a selection, deletes the selection; otherwise,
@@ -258,20 +266,15 @@ public abstract class BaseKeyListener extends MetaKeyKeyListener
    }

    // Returns the end offset to be deleted by a forward delete key from the given offset.
    private static int getOffsetForForwardDeleteKey(CharSequence text, int offset) {
    private static int getOffsetForForwardDeleteKey(CharSequence text, int offset, Paint paint) {
        final int len = text.length();

        if (offset >= len - 1) {
            return len;
        }

        int codePoint = Character.codePointAt(text, offset);
        offset += Character.charCount(codePoint);
        if (offset == len) {
            return len;
        }

        // TODO: Handle emoji, combining chars, etc.
        offset = paint.getTextRunCursor(text, offset, len, Paint.DIRECTION_LTR /* not used */,
                offset, Paint.CURSOR_AFTER);

        return adjustReplacementSpan(text, offset, false /* move to the end */);
    }
@@ -311,7 +314,18 @@ public abstract class BaseKeyListener extends MetaKeyKeyListener
        final int start = Selection.getSelectionEnd(content);
        final int end;
        if (isForwardDelete) {
            end = getOffsetForForwardDeleteKey(content, start);
            final Paint paint;
            if (view instanceof TextView) {
                paint = ((TextView)view).getPaint();
            } else {
                synchronized (mLock) {
                    if (sCachedPaint == null) {
                        sCachedPaint = new Paint();
                    }
                    paint = sCachedPaint;
                }
            }
            end = getOffsetForForwardDeleteKey(content, start, paint);
        } else {
            end = getOffsetForBackspaceKey(content, start);
        }
+472 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 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.method;

import android.app.Activity;
import android.test.suitebuilder.annotation.SmallTest;
import android.test.suitebuilder.annotation.Suppress;
import android.text.InputType;
import android.text.method.BaseKeyListener;
import android.text.method.KeyListenerTestCase;
import android.view.KeyEvent;
import android.widget.EditText;
import android.widget.TextView.BufferType;

/**
 * Test forward delete key handling of  {@link android.text.method.BaseKeyListener}.
 *
 * TODO: Move some of test cases to the CTS.
 */
public class ForwardDeleteTest extends KeyListenerTestCase {
    private static final BaseKeyListener mKeyListener = new BaseKeyListener() {
        public int getInputType() {
            return InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL;
        }
    };

    // 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());

        final KeyEvent keyEvent = getKey(KeyEvent.KEYCODE_FORWARD_DEL, modifiers);
        mActivity.runOnUiThread(new Runnable() {
            public void run() {
                mTextView.onKeyDown(keyEvent.getKeyCode(), keyEvent);
            }
        });
        mInstrumentation.waitForIdleSync();

        state.mText = mTextView.getText();
        state.mSelectionStart = mTextView.getSelectionStart();
        state.mSelectionEnd = mTextView.getSelectionEnd();
    }

    @SmallTest
    public void testSurrogatePairs() {
        EditorState state = new EditorState();

        // U+1F441 is EYE
        state.setByString("| U+1F441");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // U+1F5E8 is LEFT SPEECH BUBBLE
        state.setByString("| U+1F441 U+1F5E8");
        forwardDelete(state, 0);
        state.assertEquals("| U+1F5E8");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // TODO: introduce edge cases.
    }

    @SmallTest
    public void testReplacementSpan() {
        EditorState state = new EditorState();

        state.setByString("| 'abc' ( 'de' ) 'fg'");
        forwardDelete(state, 0);
        state.assertEquals("| 'bc' ( 'de' ) 'fg'");
        forwardDelete(state, 0);
        state.assertEquals("| 'c' ( 'de' ) 'fg'");
        forwardDelete(state, 0);
        state.assertEquals("| ( 'de' ) 'fg'");
        forwardDelete(state, 0);
        state.assertEquals("| 'fg'");
        forwardDelete(state, 0);
        state.assertEquals("| 'g'");
        forwardDelete(state, 0);
        state.assertEquals("|");

        state.setByString("'abc' [ ( 'de' ) ] 'fg'");
        forwardDelete(state, 0);
        state.assertEquals("'abc' | 'fg'");
        forwardDelete(state, 0);
        state.assertEquals("'abc' | 'g'");
        forwardDelete(state, 0);
        state.assertEquals("'abc' |");
        forwardDelete(state, 0);
        state.assertEquals("'abc' |");

        state.setByString("'ab' [ 'c' ( 'de' ) 'f' ] 'g'");
        forwardDelete(state, 0);
        state.assertEquals("'ab' | 'g'");
        forwardDelete(state, 0);
        state.assertEquals("'ab' |");
        forwardDelete(state, 0);
        state.assertEquals("'ab' |");

        // TODO: introduce edge cases.
    }

    @SmallTest
    public void testCombiningEnclosingKeycaps() {
        EditorState state = new EditorState();

        // U+20E3 is COMBINING ENCLOSING KEYCAP.
        state.setByString("| '1' U+20E3");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Edge cases
        // multiple COMBINING ENCLOSING KEYCAP
        state.setByString("| '1' U+20E3 U+20E3");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Isolated COMBINING ENCLOSING KEYCAP
        state.setByString("| U+20E3");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Isolated multiple COMBINING ENCLOSING KEYCAP
        state.setByString("| U+20E3 U+20E3");
        forwardDelete(state, 0);
        state.assertEquals("|");
    }

    @SmallTest
    public void testVariationSelector() {
        EditorState state = new EditorState();

        // U+FE0F is VARIATION SELECTOR-16.
        state.setByString("| '#' U+FE0F");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // U+E0100 is VARIATION SELECTOR-17.
        state.setByString("| U+845B U+E0100");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Edge cases
        // Isolated variation selectors
        state.setByString("| U+FE0F");
        forwardDelete(state, 0);
        state.assertEquals("|");

        state.setByString("| U+E0100");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Isolated multiple variation selectors
        state.setByString("| U+FE0F U+FE0F");
        forwardDelete(state, 0);
        state.assertEquals("|");

        state.setByString("| U+FE0F U+E0100");
        forwardDelete(state, 0);
        state.assertEquals("|");

        state.setByString("| U+E0100 U+FE0F");
        forwardDelete(state, 0);
        state.assertEquals("|");

        state.setByString("| U+E0100 U+E0100");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Multiple variation selectors
        state.setByString("| '#' U+FE0F U+FE0F");
        forwardDelete(state, 0);
        state.assertEquals("|");

        state.setByString("| '#' U+FE0F U+E0100");
        forwardDelete(state, 0);
        state.assertEquals("|");

        state.setByString("| U+845B U+E0100 U+FE0F");
        forwardDelete(state, 0);
        state.assertEquals("|");

        state.setByString("| U+845B U+E0100 U+E0100");
        forwardDelete(state, 0);
        state.assertEquals("|");
    }

    @SmallTest
    public void testEmojiZeroWidthJoinerSequence() {
        EditorState state = new EditorState();

        // U+200D is ZERO WIDTH JOINER.
        state.setByString("| U+1F441 U+200D U+1F5E8");
        forwardDelete(state, 0);
        state.assertEquals("|");

        state.setByString("| U+1F468 U+200D U+2764 U+FE0F U+200D U+1F48B U+200D U+1F468");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Edge cases
        // End with ZERO WIDTH JOINER
        state.setByString("| U+1F441 U+200D");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Start with ZERO WIDTH JOINER
        state.setByString("| U+200D U+1F5E8");
        forwardDelete(state, 0);
        state.assertEquals("| U+1F5E8");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Multiple ZERO WIDTH JOINER
        state.setByString("| U+1F441 U+200D U+200D U+1F5E8");
        forwardDelete(state, 0);
        state.assertEquals("| U+1F5E8");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Isolated ZERO WIDTH JOINER
        state.setByString("| U+200D");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Isolated multiple ZERO WIDTH JOINER
        state.setByString("| U+200D U+200D");
        forwardDelete(state, 0);
        state.assertEquals("|");
    }

    @SmallTest
    public void testFlags() {
        EditorState state = new EditorState();

        // U+1F1FA is REGIONAL INDICATOR SYMBOL LETTER U.
        // U+1F1F8 is REGIONAL INDICATOR SYMBOL LETTER S.
        state.setByString("| U+1F1FA U+1F1F8");
        forwardDelete(state, 0);
        state.assertEquals("|");

        state.setByString("| U+1F1FA U+1F1F8 U+1F1FA U+1F1F8");
        forwardDelete(state, 0);
        state.assertEquals("| U+1F1FA U+1F1F8");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Edge cases
        // Isolated regional indicator symbol
        state.setByString("| U+1F1FA");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Odd numbered regional indicator symbols
        state.setByString("| U+1F1FA U+1F1F8 U+1F1FA");
        forwardDelete(state, 0);
        state.assertEquals("| U+1F1FA");
        forwardDelete(state, 0);
        state.assertEquals("|");
    }

    @SmallTest
    public void testEmojiModifier() {
        EditorState state = new EditorState();

        // U+1F3FB is EMOJI MODIFIER FITZPATRICK TYPE-1-2.
        state.setByString("| U+1F466 U+1F3FB");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Edge cases
        // Isolated emoji modifier
        state.setByString("| U+1F3FB");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Isolated multiple emoji modifier
        state.setByString("| U+1F3FB U+1F3FB");
        forwardDelete(state, 0);
        state.assertEquals("| U+1F3FB");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Multiple emoji modifiers
        state.setByString("| U+1F466 U+1F3FB U+1F3FB");
        forwardDelete(state, 0);
        state.assertEquals("| U+1F3FB");
        forwardDelete(state, 0);
        state.assertEquals("|");
    }

    @SmallTest
    public void testMixedEdgeCases() {
        EditorState state = new EditorState();

        // COMBINING ENCLOSING KEYCAP + variation selector
        state.setByString("| '1' U+20E3 U+FE0F");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Variation selector + COMBINING ENCLOSING KEYCAP
        state.setByString("| U+2665 U+FE0F U+20E3");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // COMBINING ENCLOSING KEYCAP + ending with ZERO WIDTH JOINER
        state.setByString("| '1' U+20E3 U+200D");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // COMBINING ENCLOSING KEYCAP + ZERO WIDTH JOINER
        state.setByString("| '1' U+20E3 U+200D U+1F5E8");
        forwardDelete(state, 0);
        state.assertEquals("| U+1F5E8 ");

        // Start with ZERO WIDTH JOINER + COMBINING ENCLOSING KEYCAP
        state.setByString("| U+200D U+20E3");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // ZERO WIDTH JOINER + COMBINING ENCLOSING KEYCAP
        state.setByString("| U+1F441 U+200D U+20E3");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // COMBINING ENCLOSING KEYCAP + regional indicator symbol
        state.setByString("| '1' U+20E3 U+1F1FA");
        forwardDelete(state, 0);
        state.assertEquals("| U+1F1FA");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Regional indicator symbol + COMBINING ENCLOSING KEYCAP
        state.setByString("| U+1F1FA U+20E3");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // COMBINING ENCLOSING KEYCAP + emoji modifier
        state.setByString("| '1' U+20E3 U+1F3FB");
        forwardDelete(state, 0);
        state.assertEquals("| U+1F3FB");

        // Emoji modifier + COMBINING ENCLOSING KEYCAP
        state.setByString("| U+1F466 U+1F3FB U+20E3");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Variation selector + end with ZERO WIDTH JOINER
        state.setByString("| U+2665 U+FE0F U+200D");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Variation selector + ZERO WIDTH JOINER
        state.setByString("| U+1F469 U+200D U+2764 U+FE0F U+200D U+1F469");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Start with ZERO WIDTH JOINER + variation selector
        state.setByString("| U+200D U+FE0F");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // ZERO WIDTH JOINER + variation selector
        state.setByString("| U+1F469 U+200D U+FE0F");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Variation selector + regional indicator symbol
        state.setByString("| U+2665 U+FE0F U+1F1FA");
        forwardDelete(state, 0);
        state.assertEquals("| U+1F1FA");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Regional indicator symbol + variation selector
        state.setByString("| U+1F1FA U+FE0F");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Variation selector + emoji modifier
        state.setByString("| U+2665 U+FE0F U+1F3FB");
        forwardDelete(state, 0);
        state.assertEquals("| U+1F3FB");

        // Emoji modifier + variation selector
        state.setByString("| U+1F466 U+1F3FB U+FE0F");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Start with ZERO WIDTH JOINER + regional indicator symbol
        state.setByString("| U+200D U+1F1FA");
        forwardDelete(state, 0);
        state.assertEquals("| U+1F1FA");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // ZERO WIDTH JOINER + regional indicator symbol
        state.setByString("| U+1F469 U+200D U+1F1FA");
        forwardDelete(state, 0);
        state.assertEquals("| U+1F1FA");

        // Regional indicator symbol + end with ZERO WIDTH JOINER
        state.setByString("| U+1F1FA U+200D");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Regional indicator symbol + ZERO WIDTH JOINER
        state.setByString("| U+1F1FA U+200D U+1F469");
        forwardDelete(state, 0);
        state.assertEquals("| U+1F469");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Start with ZERO WIDTH JOINER + emoji modifier
        state.setByString("| U+200D U+1F3FB");
        forwardDelete(state, 0);
        state.assertEquals("| U+1F3FB");

        // ZERO WIDTH JOINER + emoji modifier
        state.setByString("| U+1F469 U+200D U+1F3FB");
        forwardDelete(state, 0);
        state.assertEquals("| U+1F3FB");

        // Emoji modifier + end with ZERO WIDTH JOINER
        state.setByString("| U+1F466 U+1F3FB U+200D");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Emoji modifier + ZERO WIDTH JOINER
        state.setByString("| U+1F466 U+1F3FB U+200D U+1F469");
        forwardDelete(state, 0);
        state.assertEquals("| U+1F469");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Regional indicator symbol + emoji modifier
        state.setByString("| U+1F1FA U+1F3FB");
        forwardDelete(state, 0);
        state.assertEquals("| U+1F3FB");
        forwardDelete(state, 0);
        state.assertEquals("|");

        // Emoji modifier + regional indicator symbol
        state.setByString("| U+1F466 U+1F3FB U+1F1FA");
        forwardDelete(state, 0);
        state.assertEquals("| U+1F1FA");
        forwardDelete(state, 0);
        state.assertEquals("|");
    }
}