Better support for emoji in game-text-input
Refactored game-text-input tests and test app
Test: build.sh tests
Change-Id: Ia2184e4d736fda8855e1a28b7f12f19f460fc6b9
diff --git a/build.sh b/build.sh
index bc161c8..4344754 100755
--- a/build.sh
+++ b/build.sh
@@ -38,8 +38,6 @@
fi
echo yes | $sdkmanager_path "platform-tools" "platforms;android-35" "platforms;android-31" "build-tools;35.0.0"
-# cp -Rf samples/sdk_licenses ../prebuilts/sdk/licenses
-
# Use the distribution path given to the script by the build bot in DIST_DIR. Otherwise,
# build in the default location.
if [[ -z $DIST_DIR ]]
@@ -96,6 +94,7 @@
./gradlew packageMavenZip -Plibraries=memory_advice -PdistPath="$dist_dir" -PpackageName=$package_name
./gradlew jetpadJson -Plibraries=swappy,tuningfork,game_activity,game_text_input,paddleboat,memory_advice -PdistPath="$dist_dir" -PpackageName=$package_name
fi
+
if [[ $1 != "maven-only" ]]
then
mkdir -p "$dist_dir/$package_name/apks/samples"
@@ -106,7 +105,6 @@
pushd ./samples/tuningfork/insightsdemo/
./gradlew ":app:assembleDebug"
popd
-
pushd ./samples/tuningfork/experimentsdemo/
./gradlew ":app:assembleDebug"
popd
@@ -120,7 +118,6 @@
pushd samples/bouncyball
./gradlew ":app:assembleDebug"
popd
-
pushd third_party/cube
./gradlew ":app:assembleDebug"
popd
diff --git a/game-text-input/src/androidTest/java/com/google/android/gametextinput/ExampleInstrumentedTest.java b/game-text-input/src/androidTest/java/com/google/android/gametextinput/ExampleInstrumentedTest.java
deleted file mode 100644
index 107853b..0000000
--- a/game-text-input/src/androidTest/java/com/google/android/gametextinput/ExampleInstrumentedTest.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2024 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 com.google.androidgamesdk.gametextinput.test;
-
-import static androidx.test.espresso.Espresso.onView;
-import static androidx.test.espresso.action.ViewActions.click;
-import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
-import static androidx.test.espresso.action.ViewActions.typeText;
-import static androidx.test.espresso.assertion.ViewAssertions.matches;
-import static androidx.test.espresso.matcher.ViewMatchers.withId;
-import static androidx.test.espresso.matcher.ViewMatchers.withText;
-import static org.junit.Assert.*;
-
-import androidx.test.ext.junit.rules.ActivityScenarioRule;
-import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
-import androidx.test.platform.app.InstrumentationRegistry;
-import com.google.androidgamesdk.gametextinput.test.R;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
- */
-@RunWith(AndroidJUnit4ClassRunner.class)
-public class ExampleInstrumentedTest {
- @Test
- public void testAppContext() {
- // Context of the app under test.
- android.content.Context appContext =
- InstrumentationRegistry.getInstrumentation().getTargetContext();
- assertEquals("com.google.androidgamesdk.gametextinput.test", appContext.getPackageName());
- }
-}
diff --git a/game-text-input/src/androidTest/java/com/google/android/gametextinput/InputEnabledTextView.java b/game-text-input/src/androidTest/java/com/google/android/gametextinput/InputEnabledTextView.java
index c78a6d1..da67253 100644
--- a/game-text-input/src/androidTest/java/com/google/android/gametextinput/InputEnabledTextView.java
+++ b/game-text-input/src/androidTest/java/com/google/android/gametextinput/InputEnabledTextView.java
@@ -36,6 +36,8 @@
public InputConnection mInputConnection;
private MainActivity mMainActivity;
+ private State state = new State("", 0, 0, -1, -1);
+
public InputEnabledTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@@ -68,6 +70,9 @@
}
if (outAttrs != null) {
GameTextInput.copyEditorInfo(mInputConnection.getEditorInfo(), outAttrs);
+ Log.d(LOG_TAG,
+ "onCreateInputConnection outAttrs = s:" + outAttrs.initialSelStart + "-"
+ + outAttrs.initialSelEnd);
}
return mInputConnection;
}
@@ -75,10 +80,18 @@
// Called when the IME has changed the input
@Override
public void stateChanged(State newState, boolean dismissed) {
- Log.d(LOG_TAG, "stateChanged: " + newState + " dismissed: " + dismissed);
+ state = newState;
+ Log.d(LOG_TAG,
+ "stateChanged: " + newState.text + " s: " + newState.selectionStart + "-"
+ + newState.selectionEnd + " cr: " + newState.composingRegionStart + "-"
+ + newState.composingRegionEnd);
onTextInputEvent(newState);
}
+ public State getState() {
+ return state;
+ }
+
@Override
public void onEditorAction(int action) {
Log.d(LOG_TAG, "onEditorAction: " + action);
@@ -95,11 +108,7 @@
}
private void onTextInputEvent(State state) {
- mMainActivity.setDisplayedText(state.text);
- }
-
- public void enableSoftKeyboard() {
- mInputConnection.setSoftKeyboardActive(true, 0);
+ mMainActivity.setDisplayedText(state.text, state.selectionStart, state.selectionEnd);
}
public InputConnection getInputConnection() {
diff --git a/game-text-input/src/androidTest/java/com/google/android/gametextinput/InputTest.java b/game-text-input/src/androidTest/java/com/google/android/gametextinput/InputTest.java
index d625bd1..e661b46 100644
--- a/game-text-input/src/androidTest/java/com/google/android/gametextinput/InputTest.java
+++ b/game-text-input/src/androidTest/java/com/google/android/gametextinput/InputTest.java
@@ -30,38 +30,336 @@
import static androidx.test.espresso.matcher.ViewMatchers.*;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
-import static org.junit.Assert.*;
+import android.os.SystemClock;
+import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
+import androidx.test.espresso.ViewInteraction;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import com.google.androidgamesdk.gametextinput.InputConnection;
-import com.google.androidgamesdk.gametextinput.test.MainActivity;
-import com.google.androidgamesdk.gametextinput.test.R;
-import java.util.concurrent.TimeUnit;
+import com.google.androidgamesdk.gametextinput.State;
+import com.google.common.collect.ImmutableList;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
import org.hamcrest.Matcher;
+import org.junit.After;
+import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
/**
- * Instrumented test, which will execute on an Android device.
+ * GameTextInput Instrumented tests, which execute on an Android device.
*
- * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ * To speedup test time tests are combined to groups.
*/
-@RunWith(AndroidJUnit4.class)
+@RunWith(Parameterized.class)
@LargeTest
public class InputTest {
- static private final String INITIAL_VALUE = "this is test text";
+ static private final String TAG = "InputTest";
+ enum TestGroup {
+ UNKNOWN,
+ TYPE_TEXT,
+ KEY_CHARACTER,
+ KEY_DEL,
+ KEY_FORWARD_DEL,
+ KEY_DPAD_LEFT,
+ KEY_DPAD_RIGHT,
+ KEYCODE_MOVE_HOME,
+ KEYCODE_MOVE_END
+ }
+
+ @Target({ElementType.METHOD})
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface TestToCombine {
+ TestGroup group() default TestGroup.UNKNOWN;
+ }
+
+ public enum PressKeyImplementation { PRESS_KEY, ON_KEY_LISTENER }
+ ;
+
+ @Parameter(0) public PressKeyImplementation pressKeyImplementation;
+
+ @Parameters(name = "{0}")
+ public static ImmutableList<Object[]> parameters() {
+ ImmutableList.Builder<Object[]> listBuilder = new ImmutableList.Builder<>();
+ listBuilder.add(new Object[] {PressKeyImplementation.PRESS_KEY});
+ listBuilder.add(new Object[] {PressKeyImplementation.ON_KEY_LISTENER});
+ return listBuilder.build();
+ }
@Rule
public ActivityScenarioRule<MainActivity> activityScenarioRule =
new ActivityScenarioRule<>(MainActivity.class);
+ @Before
+ public void setUp() {
+ onInputView().perform(activateSoftKeyboard());
+ }
+
+ @After
+ public void tearDown() {
+ onInputView().perform(deactivateSoftKeyboard());
+ }
+
+ @TestToCombine(group = TestGroup.TYPE_TEXT)
+ public void typeText_singleCaracter() {
+ onInputView().perform(typeText("c"));
+ checkResultText("c");
+ }
+
+ @TestToCombine(group = TestGroup.TYPE_TEXT)
+ public void typeText_phrase() {
+ onInputView().perform(typeText("123 456"));
+ checkResultText("123 456");
+ }
+
+ @TestToCombine(group = TestGroup.TYPE_TEXT)
+ public void typeText_selectAll_overwrites() {
+ onInputView().perform(typeText("abcdef"), selectText(0, 6), typeText("xyz"));
+ checkResultText("xyz");
+ }
+
+ @TestToCombine(group = TestGroup.TYPE_TEXT)
+ public void typeText_twice() {
+ onInputView().perform(typeText("abc"), typeText("def"));
+ checkResultText("abcdef");
+ }
+
+ @TestToCombine(group = TestGroup.KEY_CHARACTER)
+ public void keyCharacter_single() {
+ onInputView().perform(key(KeyEvent.KEYCODE_1));
+ checkResultText("1");
+ }
+
+ @TestToCombine(group = TestGroup.KEY_CHARACTER)
+ public void keyCharacter_sequence() {
+ onInputView().perform(key(KeyEvent.KEYCODE_1), key(KeyEvent.KEYCODE_A), key(KeyEvent.KEYCODE_2),
+ key(KeyEvent.KEYCODE_Z), key(KeyEvent.KEYCODE_9), key(KeyEvent.KEYCODE_R));
+ checkResultText("1a2z9r");
+ }
+
+ @TestToCombine(group = TestGroup.KEY_CHARACTER)
+ @Test(timeout = 100000)
+ public void keyCharacter_replaceSelection() {
+ onInputView().perform(
+ setState("BeginSelectionEnd", 5, 14), key(KeyEvent.KEYCODE_1), key(KeyEvent.KEYCODE_2));
+ checkResultText("Begin12End");
+ }
+
+ @TestToCombine(group = TestGroup.KEY_CHARACTER)
+ public void keyCharacter_replaceSelectionAll() {
+ onInputView().perform(key(KeyEvent.KEYCODE_A), key(KeyEvent.KEYCODE_B), selectText(0, 2),
+ key(KeyEvent.KEYCODE_1), key(KeyEvent.KEYCODE_2));
+ checkResultText("12");
+ }
+
+ @TestToCombine(group = TestGroup.KEY_DEL)
+ public void keyDel_atTheEnd() {
+ onInputView().perform(setState("abcdef", 6, 6), key(KeyEvent.KEYCODE_DEL));
+ checkResultText("abcde");
+ }
+
+ @TestToCombine(group = TestGroup.KEY_DEL)
+ public void keyDel_emoji() {
+ onInputView().perform(setState("abc\uD83D\uDE0Dd", 5, 5), key(KeyEvent.KEYCODE_DEL));
+ checkResultText("abcd");
+ }
+
+ @TestToCombine(group = TestGroup.KEY_DEL)
+ public void keyDel_firstCharacter() {
+ onInputView().perform(setState("abcdef", 1, 1), key(KeyEvent.KEYCODE_DEL));
+ checkResultText("bcdef");
+ }
+
+ @TestToCombine(group = TestGroup.KEY_DEL)
+ public void keyDel_middleTwoCharacters() {
+ onInputView().perform(
+ setState("123ab456", 5, 5), key(KeyEvent.KEYCODE_DEL), key(KeyEvent.KEYCODE_DEL));
+ checkResultText("123456");
+ }
+
+ @TestToCombine(group = TestGroup.KEY_DEL)
+ public void keyDel_selection() {
+ onInputView().perform(setState("123sel456", 3, 6), key(KeyEvent.KEYCODE_DEL));
+ checkResultText("123456");
+ }
+
+ @TestToCombine(group = TestGroup.KEY_DEL)
+ public void keyDel_selectAll() {
+ onInputView().perform(setState("sel", 0, 3), key(KeyEvent.KEYCODE_DEL));
+ checkResultText("");
+ }
+
+ @TestToCombine(group = TestGroup.KEY_DEL)
+ public void keyDel_atTheStart_noDeletion() {
+ onInputView().perform(setState("abc", 0, 0), key(KeyEvent.KEYCODE_DEL));
+
+ checkResultText("abc");
+ }
+
+ @TestToCombine(group = TestGroup.KEY_DEL)
+ public void keyDel_selectionTwoCharacters() {
+ onInputView().perform(setState("abcdefgh", 6, 8), key(KeyEvent.KEYCODE_DEL));
+ checkResultText("abcdef");
+ }
+
+ @TestToCombine(group = TestGroup.KEY_DEL)
+ public void keyDel_selectionFirstTwoCharacters() {
+ onInputView().perform(setState("mncdef", 0, 2), key(KeyEvent.KEYCODE_DEL), typeText("ab"));
+ checkResultText("abcdef");
+ }
+
+ @TestToCombine(group = TestGroup.KEY_DEL)
+ public void keyDel_selectionMiddle() {
+ onInputView().perform(setState("mncdef", 1, 4), key(KeyEvent.KEYCODE_DEL),
+ key(KeyEvent.KEYCODE_X), key(KeyEvent.KEYCODE_Y));
+ checkResultText("mxyef");
+ }
+
+ @TestToCombine(group = TestGroup.KEY_FORWARD_DEL)
+ public void keyForwardDel_atTheEnd_noDeletion() {
+ onInputView().perform(setState("abc", 3, 3), key(KeyEvent.KEYCODE_FORWARD_DEL));
+ checkResultText("abc");
+ }
+
+ @TestToCombine(group = TestGroup.KEY_FORWARD_DEL)
+ public void keyForwardDel_twice() {
+ onInputView().perform(setState("abcdef", 3, 3), key(KeyEvent.KEYCODE_FORWARD_DEL),
+ key(KeyEvent.KEYCODE_FORWARD_DEL));
+ checkResultText("abcf");
+ }
+
+ @TestToCombine(group = TestGroup.KEY_FORWARD_DEL)
+ public void keyForwardDel_emoji() {
+ onInputView().perform(setState("abc\uD83D\uDE0Dd", 3, 3), key(KeyEvent.KEYCODE_FORWARD_DEL));
+ checkResultText("abcd");
+ }
+
+ @TestToCombine(group = TestGroup.KEY_FORWARD_DEL)
+ public void keyForwardDel_lastCharacter() {
+ onInputView().perform(setState("abcdef", 5, 5), key(KeyEvent.KEYCODE_FORWARD_DEL));
+ checkResultText("abcde");
+ }
+
+ @TestToCombine(group = TestGroup.KEY_FORWARD_DEL)
+ public void keyForwardDel_selectAll() {
+ onInputView().perform(setState("sel", 0, 3), key(KeyEvent.KEYCODE_FORWARD_DEL));
+ checkResultText("");
+ }
+
+ @TestToCombine(group = TestGroup.KEY_FORWARD_DEL)
+ public void keyForwardDel_selectionCases() {
+ onInputView().perform(setState("abcdefgh", 6, 8), key(KeyEvent.KEYCODE_FORWARD_DEL));
+ checkResultText("abcdef");
+
+ onInputView().perform(selectText(0, 2), key(KeyEvent.KEYCODE_FORWARD_DEL), typeText("mn"));
+ checkResultText("mncdef");
+
+ onInputView().perform(selectText(1, 4), key(KeyEvent.KEYCODE_FORWARD_DEL), typeText("xyz"));
+ checkResultText("mxyzef");
+ }
+
+ @TestToCombine(group = TestGroup.KEY_DPAD_LEFT)
+ public void keyDpadLeft_middle() {
+ onInputView().perform(
+ setState("abcdefgh", 6, 6), key(KeyEvent.KEYCODE_DPAD_LEFT), key(KeyEvent.KEYCODE_X));
+
+ checkResultText("abcdexfgh");
+ }
+
+ @TestToCombine(group = TestGroup.KEY_DPAD_RIGHT)
+ public void keyDpadLeft_emoji() {
+ onInputView().perform(setState("abc\uD83D\uDE0Dd", 5, 5), key(KeyEvent.KEYCODE_DPAD_LEFT),
+ key(KeyEvent.KEYCODE_1));
+ checkResultText("abc1\uD83D\uDE0Dd");
+ }
+
+ @TestToCombine(group = TestGroup.KEY_DPAD_RIGHT)
+ public void keyDpadRight_middle() {
+ onInputView().perform(
+ setState("abcdefgh", 6, 6), key(KeyEvent.KEYCODE_DPAD_RIGHT), key(KeyEvent.KEYCODE_X));
+
+ checkResultText("abcdefgxh");
+ }
+
+ @TestToCombine(group = TestGroup.KEY_DPAD_RIGHT)
+ public void keyDpadRight_emoji() {
+ onInputView().perform(setState("abc\uD83D\uDE0Dd", 3, 3), key(KeyEvent.KEYCODE_DPAD_RIGHT),
+ key(KeyEvent.KEYCODE_1));
+ checkResultText("abc\uD83D\uDE0D1d");
+ }
+
+ @TestToCombine(group = TestGroup.KEYCODE_MOVE_HOME)
+ public void keyMoveHome_fromMiddle() {
+ onInputView().perform(setState("abcdefgh", 3, 3), key(KeyEvent.KEYCODE_1),
+ key(KeyEvent.KEYCODE_MOVE_HOME), key(KeyEvent.KEYCODE_2));
+ checkResultText("2abc1defgh");
+ }
+
+ @TestToCombine(group = TestGroup.KEYCODE_MOVE_END)
+ public void keyMoveEnd_fromFirstChar() {
+ onInputView().perform(setState("abcdefgh", 0, 0), key(KeyEvent.KEYCODE_1),
+ key(KeyEvent.KEYCODE_MOVE_END), key(KeyEvent.KEYCODE_2));
+ checkResultText("1abcdefgh2");
+ }
+
+ @Test
+ public void runCombinedTests_type() {
+ runGroupTests(TestGroup.TYPE_TEXT, this.getClass());
+ runGroupTests(TestGroup.KEY_CHARACTER, this.getClass());
+ }
+
+ @Test
+ public void runCombinedTests_DeletionKeys() {
+ runGroupTests(TestGroup.KEY_DEL, this.getClass());
+ runGroupTests(TestGroup.KEY_FORWARD_DEL, this.getClass());
+ }
+
+ @Test
+ public void runCombinedTests_DpadKeys() {
+ runGroupTests(TestGroup.KEY_DPAD_LEFT, this.getClass());
+ runGroupTests(TestGroup.KEY_DPAD_RIGHT, this.getClass());
+ runGroupTests(TestGroup.KEYCODE_MOVE_HOME, this.getClass());
+ runGroupTests(TestGroup.KEYCODE_MOVE_END, this.getClass());
+ }
+
+ private ViewInteraction onInputView() {
+ return onView(withId(R.id.input_enabled_text_view));
+ }
+
+ private void resetBeforeTest() {
+ onInputView().perform(setState("", 0, 0));
+ }
+
+ private void checkResultText(String text) {
+ onInputView().perform(waitForIdle());
+ onView(withId(R.id.displayed_text)).check(matches(withText(text)));
+ }
+
+ private ViewAction key(int keyCode) {
+ if (pressKeyImplementation == PressKeyImplementation.PRESS_KEY) {
+ return pressKey(keyCode);
+ }
+ if (pressKeyImplementation == PressKeyImplementation.ON_KEY_LISTENER) {
+ return passKeyToOnKeyListener(keyCode);
+ }
+ return null;
+ }
+
// Espresso doesn't support selecting text, so this action implements this ability.
// Note that start = end means in Android this is just a text cursor.
// For example, start = end = 3 means that there is no selection and the cursor
@@ -81,272 +379,166 @@
@Override
public void perform(UiController uiController, View view) {
InputEnabledTextView inputView = (InputEnabledTextView) view;
- InputConnection ic = inputView.getInputConnection();
+ State state = inputView.getState();
+ state.selectionStart = start;
+ state.selectionEnd = end;
- ic.setSelection(start, end);
+ InputConnection ic = inputView.getInputConnection();
+ ic.setState(state);
+ uiController.loopMainThreadForAtLeast(500);
}
};
}
- //////////////////////////////////////////////////////////////////////////
- // Below are tests. First group of tests is for simple text typing.
- //////////////////////////////////////////////////////////////////////////
+ private ViewAction sleep(int time) {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return isDisplayed();
+ }
- @Test
- public void canTypeCharOnSoftwareKeyboard() {
- onView(withId(R.id.displayed_text)).check(matches(withText(INITIAL_VALUE)));
+ @Override
+ public String getDescription() {
+ return "sleep";
+ }
- activityScenarioRule.getScenario().onActivity(activity -> activity.enableSoftKeyboard());
-
- onView(withId(R.id.input_enabled_text_view)).perform(typeText("s"), closeSoftKeyboard());
-
- onView(withId(R.id.displayed_text)).check(matches(withText("s")));
+ @Override
+ public void perform(UiController uiController, View view) {
+ uiController.loopMainThreadForAtLeast(time);
+ }
+ };
}
- @Test
- public void canTypeTextOnSoftwareKeyboard() {
- onView(withId(R.id.displayed_text)).check(matches(withText(INITIAL_VALUE)));
+ private ViewAction activateSoftKeyboard() {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return isDisplayed();
+ }
- activityScenarioRule.getScenario().onActivity(activity -> activity.enableSoftKeyboard());
+ @Override
+ public String getDescription() {
+ return "activateSoftKeyboard";
+ }
- onView(withId(R.id.input_enabled_text_view)).perform(typeText("abcdef"), closeSoftKeyboard());
-
- onView(withId(R.id.displayed_text)).check(matches(withText("abcdef")));
+ @Override
+ public void perform(UiController uiController, View view) {
+ InputEnabledTextView inputView = (InputEnabledTextView) view;
+ InputConnection ic = inputView.getInputConnection();
+ ic.setSoftKeyboardActive(true, 0);
+ uiController.loopMainThreadForAtLeast(1000);
+ }
+ };
}
- @Test
- public void canTypeCharOnHardwareKeyboard() {
- onView(withId(R.id.displayed_text)).check(matches(withText(INITIAL_VALUE)));
+ private ViewAction deactivateSoftKeyboard() {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return isDisplayed();
+ }
- activityScenarioRule.getScenario().onActivity(activity -> activity.enableSoftKeyboard());
+ @Override
+ public String getDescription() {
+ return "deactivateSoftKeyboard";
+ }
- onView(withId(R.id.input_enabled_text_view))
- .perform(pressKey(KeyEvent.KEYCODE_1), closeSoftKeyboard());
-
- onView(withId(R.id.displayed_text)).check(matches(withText("1")));
+ @Override
+ public void perform(UiController uiController, View view) {
+ InputEnabledTextView inputView = (InputEnabledTextView) view;
+ InputConnection ic = inputView.getInputConnection();
+ ic.setSoftKeyboardActive(false, 0);
+ }
+ };
}
- @Test
- public void canTypeTextOnHardwareKeyboard() {
- onView(withId(R.id.displayed_text)).check(matches(withText(INITIAL_VALUE)));
+ private ViewAction waitForIdle() {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return isDisplayed();
+ }
- activityScenarioRule.getScenario().onActivity(activity -> activity.enableSoftKeyboard());
+ @Override
+ public String getDescription() {
+ return "waitForIdle";
+ }
- onView(withId(R.id.input_enabled_text_view))
- .perform(pressKey(KeyEvent.KEYCODE_1), pressKey(KeyEvent.KEYCODE_A),
- pressKey(KeyEvent.KEYCODE_2), pressKey(KeyEvent.KEYCODE_Z),
- pressKey(KeyEvent.KEYCODE_9), pressKey(KeyEvent.KEYCODE_R), closeSoftKeyboard());
-
- onView(withId(R.id.displayed_text)).check(matches(withText("1a2z9r")));
+ @Override
+ public void perform(UiController uiController, View view) {
+ uiController.loopMainThreadUntilIdle();
+ }
+ };
}
- //////////////////////////////////////////////////////////////////////////
- // Tests for Backspace and Delete buttons - single character.
- //////////////////////////////////////////////////////////////////////////
+ private ViewAction setState(String text, int selectionStart, int selectionEnd) {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return isDisplayed();
+ }
- @Test
- public void hardwareBackspaceAtTheEndWorks() {
- onView(withId(R.id.displayed_text)).check(matches(withText(INITIAL_VALUE)));
+ @Override
+ public String getDescription() {
+ return "setState to: " + text + ", s: " + selectionStart + "-" + selectionEnd;
+ }
- activityScenarioRule.getScenario().onActivity(activity -> activity.enableSoftKeyboard());
-
- onView(withId(R.id.input_enabled_text_view))
- .perform(typeText("abcdef"), pressKey(KeyEvent.KEYCODE_DEL), closeSoftKeyboard());
-
- onView(withId(R.id.displayed_text)).check(matches(withText("abcde")));
+ @Override
+ public void perform(UiController uiController, View view) {
+ InputEnabledTextView inputView = (InputEnabledTextView) view;
+ InputConnection ic = inputView.getInputConnection();
+ State state = new State(text, selectionStart, selectionEnd, -1, -1);
+ ic.setState(state);
+ uiController.loopMainThreadForAtLeast(500);
+ }
+ };
}
- @Test
- public void hardwareBackspaceAtTheStartDoesNothing() {
- onView(withId(R.id.displayed_text)).check(matches(withText(INITIAL_VALUE)));
+ private ViewAction passKeyToOnKeyListener(int keyCode) {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return isDisplayed();
+ }
- activityScenarioRule.getScenario().onActivity(activity -> activity.enableSoftKeyboard());
+ @Override
+ public String getDescription() {
+ return "Select text";
+ }
- onView(withId(R.id.input_enabled_text_view))
- .perform(
- typeText("abc"), selectText(0, 0), pressKey(KeyEvent.KEYCODE_DEL), closeSoftKeyboard());
-
- onView(withId(R.id.displayed_text)).check(matches(withText("abc")));
+ @Override
+ public void perform(UiController uiController, View view) {
+ InputEnabledTextView inputView = (InputEnabledTextView) view;
+ InputConnection ic = inputView.getInputConnection();
+ long eventTime = SystemClock.uptimeMillis();
+ KeyEvent downEvent =
+ new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, keyCode, 0, 0);
+ ic.onKey(view, keyCode, downEvent);
+ KeyEvent upEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, keyCode, 0, 0);
+ ic.onKey(view, keyCode, upEvent);
+ }
+ };
}
- @Test
- public void hardwareDeleteAtTheEndDoesNothing() {
- onView(withId(R.id.displayed_text)).check(matches(withText(INITIAL_VALUE)));
+ public void runGroupTests(TestGroup testGroup, Class<?> testClass) {
+ List<Method> testsToRun = new ArrayList<>();
+ for (Method method : testClass.getDeclaredMethods()) {
+ if (method.isAnnotationPresent(TestToCombine.class)) {
+ TestToCombine annotation = method.getAnnotation(TestToCombine.class);
+ if (annotation.group().equals(testGroup)) {
+ testsToRun.add(method);
+ }
+ }
+ }
- activityScenarioRule.getScenario().onActivity(activity -> activity.enableSoftKeyboard());
-
- onView(withId(R.id.input_enabled_text_view))
- .perform(typeText("abc"), selectText(3, 3), pressKey(KeyEvent.KEYCODE_FORWARD_DEL),
- closeSoftKeyboard());
-
- onView(withId(R.id.displayed_text)).check(matches(withText("abc")));
- }
-
- @Test
- public void hardwareBackspaceInTheMiddleWorks() {
- onView(withId(R.id.displayed_text)).check(matches(withText(INITIAL_VALUE)));
-
- activityScenarioRule.getScenario().onActivity(activity -> activity.enableSoftKeyboard());
-
- onView(withId(R.id.input_enabled_text_view))
- .perform(typeText("abcdef"), selectText(1, 1), pressKey(KeyEvent.KEYCODE_DEL));
-
- onView(withId(R.id.displayed_text)).check(matches(withText("bcdef")));
-
- onView(withId(R.id.input_enabled_text_view))
- .perform(selectText(3, 3), pressKey(KeyEvent.KEYCODE_DEL), pressKey(KeyEvent.KEYCODE_DEL));
-
- onView(withId(R.id.displayed_text)).check(matches(withText("bef")));
-
- onView(withId(R.id.input_enabled_text_view)).perform(typeText("xyz"));
-
- onView(withId(R.id.displayed_text)).check(matches(withText("bxyzef")));
- }
-
- @Test
- public void multipleHardwareDeletesWork() {
- onView(withId(R.id.displayed_text)).check(matches(withText(INITIAL_VALUE)));
-
- activityScenarioRule.getScenario().onActivity(activity -> activity.enableSoftKeyboard());
-
- onView(withId(R.id.input_enabled_text_view))
- .perform(typeText("abcde"), selectText(3, 3), pressKey(KeyEvent.KEYCODE_FORWARD_DEL),
- pressKey(KeyEvent.KEYCODE_FORWARD_DEL));
-
- onView(withId(R.id.displayed_text)).check(matches(withText("abc")));
- }
-
- @Test
- public void hardwareDeleteInTheMiddleWorks() {
- onView(withId(R.id.displayed_text)).check(matches(withText(INITIAL_VALUE)));
-
- activityScenarioRule.getScenario().onActivity(activity -> activity.enableSoftKeyboard());
-
- onView(withId(R.id.input_enabled_text_view))
- .perform(typeText("abcdef"), selectText(1, 1), pressKey(KeyEvent.KEYCODE_FORWARD_DEL));
-
- onView(withId(R.id.displayed_text)).check(matches(withText("acdef")));
-
- onView(withId(R.id.input_enabled_text_view))
- .perform(selectText(3, 3), pressKey(KeyEvent.KEYCODE_FORWARD_DEL),
- pressKey(KeyEvent.KEYCODE_FORWARD_DEL));
-
- onView(withId(R.id.displayed_text)).check(matches(withText("acd")));
-
- onView(withId(R.id.input_enabled_text_view)).perform(selectText(1, 1), typeText("xyz"));
-
- onView(withId(R.id.displayed_text)).check(matches(withText("axyzcd")));
- }
-
- //////////////////////////////////////////////////////////////////////////
- // Tests for deletion of selected text.
- //////////////////////////////////////////////////////////////////////////
-
- @Test
- public void selectAllWithOverwriteWorks() {
- onView(withId(R.id.displayed_text)).check(matches(withText(INITIAL_VALUE)));
-
- activityScenarioRule.getScenario().onActivity(activity -> activity.enableSoftKeyboard());
-
- onView(withId(R.id.input_enabled_text_view))
- .perform(typeText("abcdef"), selectText(0, 6), typeText("xyz"), closeSoftKeyboard());
-
- onView(withId(R.id.displayed_text)).check(matches(withText("xyz")));
- }
-
- @Test
- public void selectAllWithHardwareBackspaceWorks() {
- onView(withId(R.id.displayed_text)).check(matches(withText(INITIAL_VALUE)));
-
- activityScenarioRule.getScenario().onActivity(activity -> activity.enableSoftKeyboard());
-
- onView(withId(R.id.input_enabled_text_view))
- .perform(typeText("abcdef"), selectText(0, 6), pressKey(KeyEvent.KEYCODE_DEL),
- closeSoftKeyboard());
-
- onView(withId(R.id.displayed_text)).check(matches(withText("")));
- }
-
- @Test
- public void selectAllWithHardwareDeleteWorks() {
- onView(withId(R.id.displayed_text)).check(matches(withText(INITIAL_VALUE)));
-
- activityScenarioRule.getScenario().onActivity(activity -> activity.enableSoftKeyboard());
-
- onView(withId(R.id.input_enabled_text_view))
- .perform(typeText("abcdef"), selectText(0, 6), pressKey(KeyEvent.KEYCODE_FORWARD_DEL),
- closeSoftKeyboard());
-
- onView(withId(R.id.displayed_text)).check(matches(withText("")));
- }
-
- @Test
- public void hardwareBackspaceWorksWithAnySelection() {
- onView(withId(R.id.displayed_text)).check(matches(withText(INITIAL_VALUE)));
-
- activityScenarioRule.getScenario().onActivity(activity -> activity.enableSoftKeyboard());
-
- onView(withId(R.id.input_enabled_text_view))
- .perform(typeText("abcdefgh"), selectText(6, 8), pressKey(KeyEvent.KEYCODE_DEL));
-
- onView(withId(R.id.displayed_text)).check(matches(withText("abcdef")));
-
- onView(withId(R.id.input_enabled_text_view))
- .perform(selectText(0, 2), pressKey(KeyEvent.KEYCODE_DEL), typeText("mn"));
-
- onView(withId(R.id.displayed_text)).check(matches(withText("mncdef")));
-
- onView(withId(R.id.input_enabled_text_view))
- .perform(selectText(1, 4), pressKey(KeyEvent.KEYCODE_DEL), typeText("xyz"));
-
- onView(withId(R.id.displayed_text)).check(matches(withText("mxyzef")));
- }
-
- @Test
- public void hardwareDeleteWorksWithAnySelection() {
- onView(withId(R.id.displayed_text)).check(matches(withText(INITIAL_VALUE)));
-
- activityScenarioRule.getScenario().onActivity(activity -> activity.enableSoftKeyboard());
-
- onView(withId(R.id.input_enabled_text_view))
- .perform(typeText("abcdefgh"), selectText(6, 8), pressKey(KeyEvent.KEYCODE_FORWARD_DEL));
-
- onView(withId(R.id.displayed_text)).check(matches(withText("abcdef")));
-
- onView(withId(R.id.input_enabled_text_view))
- .perform(selectText(0, 2), pressKey(KeyEvent.KEYCODE_FORWARD_DEL), typeText("mn"));
-
- onView(withId(R.id.displayed_text)).check(matches(withText("mncdef")));
-
- onView(withId(R.id.input_enabled_text_view))
- .perform(selectText(1, 4), pressKey(KeyEvent.KEYCODE_FORWARD_DEL), typeText("xyz"));
-
- onView(withId(R.id.displayed_text)).check(matches(withText("mxyzef")));
- }
-
- @Test
- public void pressingLeftMovesCursorLeft() {
- onView(withId(R.id.displayed_text)).check(matches(withText(INITIAL_VALUE)));
-
- activityScenarioRule.getScenario().onActivity(activity -> activity.enableSoftKeyboard());
-
- onView(withId(R.id.input_enabled_text_view))
- .perform(typeText("abcdefgh"), selectText(6, 6), pressKey(KeyEvent.KEYCODE_DPAD_LEFT),
- pressKey(KeyEvent.KEYCODE_X));
-
- onView(withId(R.id.displayed_text)).check(matches(withText("abcdexfgh")));
- }
-
- @Test
- public void pressingRightMovesCursorRight() {
- onView(withId(R.id.displayed_text)).check(matches(withText(INITIAL_VALUE)));
-
- activityScenarioRule.getScenario().onActivity(activity -> activity.enableSoftKeyboard());
-
- onView(withId(R.id.input_enabled_text_view))
- .perform(typeText("abcdefgh"), selectText(6, 6), pressKey(KeyEvent.KEYCODE_DPAD_RIGHT),
- pressKey(KeyEvent.KEYCODE_X));
-
- onView(withId(R.id.displayed_text)).check(matches(withText("abcdefgxh")));
+ for (Method test : testsToRun) {
+ Log.d(TAG, "Executing test: " + test.getName());
+ try {
+ resetBeforeTest();
+ test.invoke(this);
+ } catch (InvocationTargetException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
}
}
diff --git a/game-text-input/src/androidTest/java/com/google/android/gametextinput/MainActivity.java b/game-text-input/src/androidTest/java/com/google/android/gametextinput/MainActivity.java
index d48109e..3aaafaf 100644
--- a/game-text-input/src/androidTest/java/com/google/android/gametextinput/MainActivity.java
+++ b/game-text-input/src/androidTest/java/com/google/android/gametextinput/MainActivity.java
@@ -15,15 +15,18 @@
*/
package com.google.androidgamesdk.gametextinput.test;
+import android.graphics.Color;
import android.os.Bundle;
import android.text.InputType;
-import android.view.inputmethod.EditorInfo;
-import android.widget.Button;
+import android.text.SpannableString;
+import android.text.style.BackgroundColorSpan;
+import android.util.Log;
import android.widget.TextView;
+import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
-import com.google.androidgamesdk.gametextinput.test.R;
-
-// import com.google.androidgamesdk.gametextinput.InputConnection;
+import androidx.core.graphics.Insets;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
public class MainActivity extends AppCompatActivity {
InputEnabledTextView inputEnabledTextView;
@@ -32,8 +35,9 @@
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
-
+ applyInsets();
inputEnabledTextView = (InputEnabledTextView) findViewById(R.id.input_enabled_text_view);
assert (inputEnabledTextView != null);
@@ -43,15 +47,20 @@
inputEnabledTextView.createInputConnection(InputType.TYPE_CLASS_TEXT, this);
}
- public void setDisplayedText(String text) {
- displayedText.setText(text);
+ public void setDisplayedText(String text, int selectionStart, int selectionEnd) {
+ SpannableString str = new SpannableString(text);
+
+ if (selectionStart != selectionEnd) {
+ str.setSpan(new BackgroundColorSpan(Color.YELLOW), selectionStart, selectionEnd, 0);
+ }
+ displayedText.setText(str);
}
- public String getTestString() {
- return "abc";
- }
-
- public void enableSoftKeyboard() {
- inputEnabledTextView.enableSoftKeyboard();
+ private void applyInsets() {
+ ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
+ Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
+ v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
+ return insets;
+ });
}
}
diff --git a/game-text-input/src/androidTest/res/layout/activity_main.xml b/game-text-input/src/androidTest/res/layout/activity_main.xml
index 0b77257..07e042b 100644
--- a/game-text-input/src/androidTest/res/layout/activity_main.xml
+++ b/game-text-input/src/androidTest/res/layout/activity_main.xml
@@ -1,24 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
- <com.google.androidgamesdk.gametextinput.test.InputEnabledTextView
- android:id="@+id/input_enabled_text_view"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="this is test input"
- />
-
<TextView
android:id="@+id/displayed_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginTop="16dp"
- android:text="this is test text"
- android:textAppearance="@style/TextAppearance.AppCompat.Large" />
+ android:textAppearance="@style/TextAppearance.AppCompat.Large"
+ android:text="this is test text"/>
+ <com.google.androidgamesdk.gametextinput.test.InputEnabledTextView
+ android:id="@+id/input_enabled_text_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintTop_toBottomOf="@+id/displayed_text"
+ />
-</LinearLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/game-text-input/src/androidTest/res/values-night/styles.xml b/game-text-input/src/androidTest/res/values-night/styles.xml
deleted file mode 100644
index f9e288b..0000000
--- a/game-text-input/src/androidTest/res/values-night/styles.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<resources>
-
- <style name="Widget.Theme.GameTextInputTest.MyView" parent="">
- <item name="android:background">@color/gray_600</item>
- <item name="exampleColor">@color/light_blue_600</item>
- </style>
-</resources>
diff --git a/game-text-input/src/androidTest/res/values-night/themes.xml b/game-text-input/src/androidTest/res/values-night/themes.xml
index 694e50d..6cc788f 100644
--- a/game-text-input/src/androidTest/res/values-night/themes.xml
+++ b/game-text-input/src/androidTest/res/values-night/themes.xml
@@ -1,16 +1,4 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
- <style name="Theme.GameTextInputTest" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
- <!-- Primary brand color. -->
- <item name="colorPrimary">@color/purple_200</item>
- <item name="colorPrimaryVariant">@color/purple_700</item>
- <item name="colorOnPrimary">@color/black</item>
- <!-- Secondary brand color. -->
- <item name="colorSecondary">@color/teal_200</item>
- <item name="colorSecondaryVariant">@color/teal_200</item>
- <item name="colorOnSecondary">@color/black</item>
- <!-- Status bar color. -->
- <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
- <!-- Customize your theme here. -->
- </style>
+ <style name="Theme.GameTextInputTest" parent="Theme.Material3.DayNight"/>
</resources>
diff --git a/game-text-input/src/androidTest/res/values/attrs_input_enabled_text_view.xml b/game-text-input/src/androidTest/res/values/attrs_input_enabled_text_view.xml
deleted file mode 100644
index d5523d5..0000000
--- a/game-text-input/src/androidTest/res/values/attrs_input_enabled_text_view.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<resources>
- <declare-styleable name="InputEnabledTextView">
- <attr name="exampleString" format="string" />
- <attr name="exampleDimension" format="dimension" />
- <attr name="exampleColor" format="color" />
- <attr name="exampleDrawable" format="color|reference" />
- </declare-styleable>
-</resources>
\ No newline at end of file
diff --git a/game-text-input/src/androidTest/res/values/colors.xml b/game-text-input/src/androidTest/res/values/colors.xml
deleted file mode 100644
index 66e3f6c..0000000
--- a/game-text-input/src/androidTest/res/values/colors.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
- <color name="purple_200">#FFBB86FC</color>
- <color name="purple_500">#FF6200EE</color>
- <color name="purple_700">#FF3700B3</color>
- <color name="teal_200">#FF03DAC5</color>
- <color name="teal_700">#FF018786</color>
- <color name="black">#FF000000</color>
- <color name="white">#FFFFFFFF</color>
- <color name="light_blue_400">#FF29B6F6</color>
- <color name="light_blue_600">#FF039BE5</color>
- <color name="gray_400">#FFBDBDBD</color>
- <color name="gray_600">#FF757575</color>
-</resources>
\ No newline at end of file
diff --git a/game-text-input/src/androidTest/res/values/styles.xml b/game-text-input/src/androidTest/res/values/styles.xml
deleted file mode 100644
index b89766f..0000000
--- a/game-text-input/src/androidTest/res/values/styles.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<resources>
-
- <style name="Widget.Theme.GameTextInputTest.MyView" parent="">
- <item name="android:background">@color/gray_400</item>
- <item name="exampleColor">@color/light_blue_400</item>
- </style>
-</resources>
diff --git a/game-text-input/src/androidTest/res/values/themes.xml b/game-text-input/src/androidTest/res/values/themes.xml
index e82ce08..6cc788f 100644
--- a/game-text-input/src/androidTest/res/values/themes.xml
+++ b/game-text-input/src/androidTest/res/values/themes.xml
@@ -1,16 +1,4 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
- <style name="Theme.GameTextInputTest" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
- <!-- Primary brand color. -->
- <item name="colorPrimary">@color/purple_500</item>
- <item name="colorPrimaryVariant">@color/purple_700</item>
- <item name="colorOnPrimary">@color/white</item>
- <!-- Secondary brand color. -->
- <item name="colorSecondary">@color/teal_200</item>
- <item name="colorSecondaryVariant">@color/teal_700</item>
- <item name="colorOnSecondary">@color/black</item>
- <!-- Status bar color. -->
- <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
- <!-- Customize your theme here. -->
- </style>
+ <style name="Theme.GameTextInputTest" parent="Theme.Material3.DayNight"/>
</resources>
diff --git a/game-text-input/src/main/java/com/google/androidgamesdk/gametextinput/InputConnection.java b/game-text-input/src/main/java/com/google/androidgamesdk/gametextinput/InputConnection.java
index c70a297..c95f468 100644
--- a/game-text-input/src/main/java/com/google/androidgamesdk/gametextinput/InputConnection.java
+++ b/game-text-input/src/main/java/com/google/androidgamesdk/gametextinput/InputConnection.java
@@ -124,7 +124,7 @@
* after calling setEditorInfo.
*/
public void restartInput() {
- this.imm.restartInput(targetView);
+ imm.restartInput(targetView);
}
/**
@@ -153,8 +153,8 @@
this.imm.showSoftInput(this.targetView, flags);
} else {
this.imm.hideSoftInputFromWindow(this.targetView.getWindowToken(), flags);
- imm.restartInput(targetView);
}
+ restartInput();
}
/**
@@ -173,7 +173,7 @@
*/
public final void setEditorInfo(EditorInfo editorInfo) {
Log.d(TAG, "setEditorInfo");
- this.settings.mEditorInfo = editorInfo;
+ settings.mEditorInfo = editorInfo;
// Depending on the multiline state, we might need a different set of filters.
// Filters are being used to filter specific characters for hardware keyboards
@@ -201,10 +201,13 @@
+ state.selectionEnd + "), composing region=(" + state.composingRegionStart + ","
+ state.composingRegionEnd + ")");
mEditable.clear();
+ mEditable.clearSpans();
mEditable.insert(0, (CharSequence) state.text);
setSelection(state.selectionStart, state.selectionEnd);
- setComposingRegion(state.composingRegionStart, state.composingRegionEnd);
- informIMM();
+ if (state.composingRegionStart != state.composingRegionEnd) {
+ setComposingRegion(state.composingRegionStart, state.composingRegionEnd);
+ }
+ restartInput();
}
/**
@@ -235,7 +238,15 @@
return false;
}
// Don't call sendKeyEvent as it might produce an infinite loop.
- return processKeyEvent(keyEvent);
+ if (processKeyEvent(keyEvent)) {
+ // IMM seems to cache the content of Editable, so we update it with restartInput
+ // Also it caches selection and composing region, so let's notify it about updates.
+ stateUpdated();
+ immUpdateSelection();
+ restartInput();
+ return true;
+ }
+ return false;
}
// From BaseInputConnection
@@ -263,7 +274,6 @@
return super.setComposingText(text, newCursorPosition);
}
- // From BaseInputConnection
@Override
public boolean setComposingRegion(int start, int end) {
Log.d(TAG, "setComposingRegion: " + start + ":" + end);
@@ -395,14 +405,15 @@
return super.performPrivateCommand(action, data);
}
- private void informIMM() {
+ private void immUpdateSelection() {
Pair selection = this.getSelection();
Pair cr = this.getComposingRegion();
Log.d(TAG,
- "informIMM: " + selection.first + "," + selection.second + ". " + cr.first + ","
+ "immUpdateSelection: " + selection.first + "," + selection.second + ". " + cr.first + ","
+ cr.second);
- this.imm.updateSelection(
- this.targetView, selection.first, selection.second, cr.first, cr.second);
+ settings.mEditorInfo.initialSelStart = selection.first;
+ settings.mEditorInfo.initialSelEnd = selection.second;
+ imm.updateSelection(targetView, selection.first, selection.second, cr.first, cr.second);
}
private Pair getSelection() {
@@ -417,19 +428,18 @@
if (event == null) {
return false;
}
- Log.d(TAG,
- String.format(
- "processKeyEvent(key=%d) text=%s", event.getKeyCode(), this.mEditable.toString()));
+ int keyCode = event.getKeyCode();
+ Log.d(
+ TAG, String.format("processKeyEvent(key=%d) text=%s", keyCode, this.mEditable.toString()));
// Filter out Enter keys if multi-line mode is disabled.
if ((settings.mEditorInfo.inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) == 0
- && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER
- || event.getKeyCode() == KeyEvent.KEYCODE_NUMPAD_ENTER)
+ && (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER)
&& event.hasNoModifiers()) {
sendEditorAction(settings.mEditorInfo.actionId);
return true;
}
- if (event.getAction() != 0) {
- return true;
+ if (event.getAction() != KeyEvent.ACTION_DOWN) {
+ return false;
}
// If no selection is set, move the selection to the end.
// This is the case when first typing on keys when the selection is not set.
@@ -441,75 +451,83 @@
selection.second = this.mEditable.length();
}
- boolean modified = false;
-
- if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_LEFT) {
+ if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
if (selection.first == selection.second) {
- setSelection(selection.first - 1, selection.second - 1);
+ int newIndex = findIndexBackward(mEditable, selection.first, 1);
+ setSelection(newIndex, newIndex);
} else {
setSelection(selection.first, selection.first);
}
return true;
- } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_RIGHT) {
+ }
+ if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
if (selection.first == selection.second) {
- setSelection(selection.first + 1, selection.second + 1);
+ int newIndex = findIndexForward(mEditable, selection.second, 1);
+ setSelection(newIndex, newIndex);
} else {
setSelection(selection.second, selection.second);
}
return true;
- } else if (selection.first != selection.second) {
- Log.d(TAG, String.format("processKeyEvent: deleting selection"));
- this.mEditable.delete(selection.first, selection.second);
- modified = true;
- } else if (event.getKeyCode() == KeyEvent.KEYCODE_DEL && selection.first > 0) {
- this.mEditable.delete(selection.first - 1, selection.first);
- this.stateUpdated();
- Log.d(TAG,
- String.format("processKeyEvent: exit after DEL, text=%s", this.mEditable.toString()));
- return true;
- } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL
- && selection.first < this.mEditable.length()) {
- this.mEditable.delete(selection.first, selection.first + 1);
- this.stateUpdated();
- Log.d(TAG,
- String.format(
- "processKeyEvent: exit after FORWARD_DEL, text=%s", this.mEditable.toString()));
+ }
+ if (keyCode == KeyEvent.KEYCODE_MOVE_HOME) {
+ setSelection(0, 0);
return true;
}
-
- int code = event.getKeyCode();
- if (event.getUnicodeChar() != 0) {
- String charsToInsert = Character.toString((char) event.getUnicodeChar());
- this.mEditable.insert(selection.first, (CharSequence) charsToInsert);
- int length = this.mEditable.length();
-
- // Same logic as in setComposingText(): we must update composing region,
- // so make sure it points to a valid range.
- Pair composingRegion = this.getComposingRegion();
- if (composingRegion.first == -1) {
- composingRegion = this.getSelection();
- if (composingRegion.first == -1) {
- composingRegion = new Pair(0, 0);
+ if (keyCode == KeyEvent.KEYCODE_MOVE_END) {
+ setSelection(this.mEditable.length(), this.mEditable.length());
+ return true;
+ }
+ if (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
+ if (selection.first != selection.second) {
+ this.mEditable.delete(selection.first, selection.second);
+ return true;
+ }
+ if (keyCode == KeyEvent.KEYCODE_DEL) {
+ if (selection.first > 0) {
+ finishComposingText();
+ deleteSurroundingTextInCodePoints(1, 0);
+ return true;
}
}
-
- // IMM seems to cache the content of Editable, so we update it with restartInput
- // Also it caches selection and composing region, so let's notify it about updates.
- composingRegion.second = composingRegion.first + length;
- this.setComposingRegion(composingRegion.first, composingRegion.second);
- int new_cursor = selection.first + charsToInsert.length();
- setSelection(new_cursor, new_cursor);
- this.informIMM();
- this.restartInput();
- modified = true;
+ if (keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
+ if (selection.first < this.mEditable.length()) {
+ finishComposingText();
+ deleteSurroundingTextInCodePoints(0, 1);
+ return true;
+ }
+ }
+ return false;
}
- if (modified) {
- Log.d(TAG, String.format("processKeyEvent: exit, text=%s", this.mEditable.toString()));
- this.stateUpdated();
+ if (event.getUnicodeChar() == 0) {
+ return false;
}
- return modified;
+ if (selection.first != selection.second) {
+ Log.d(TAG, String.format("processKeyEvent: deleting selection"));
+ this.mEditable.delete(selection.first, selection.second);
+ }
+
+ String charsToInsert = Character.toString((char) event.getUnicodeChar());
+ this.mEditable.insert(selection.first, (CharSequence) charsToInsert);
+ int length = this.mEditable.length();
+
+ // Same logic as in setComposingText(): we must update composing region,
+ // so make sure it points to a valid range.
+ Pair composingRegion = this.getComposingRegion();
+ if (composingRegion.first == -1) {
+ composingRegion = this.getSelection();
+ if (composingRegion.first == -1) {
+ composingRegion = new Pair(0, 0);
+ }
+ }
+
+ composingRegion.second = composingRegion.first + length;
+ this.setComposingRegion(composingRegion.first, composingRegion.second);
+ int new_cursor = selection.first + charsToInsert.length();
+ setSelection(new_cursor, new_cursor);
+ Log.d(TAG, String.format("processKeyEvent: exit, text=%s", this.mEditable.toString()));
+ return true;
}
private final void stateUpdated() {
@@ -517,6 +535,8 @@
Pair cr = this.getComposingRegion();
State state = new State(
this.mEditable.toString(), selection.first, selection.second, cr.first, cr.second);
+ settings.mEditorInfo.initialSelStart = selection.first;
+ settings.mEditorInfo.initialSelEnd = selection.second;
// Keep a reference to the listener to avoid a race condition when setting the listener.
Listener listener = this.listener;
@@ -597,4 +617,98 @@
}
return false;
}
+
+ private static int INVALID_INDEX = -1;
+ // Implementation copy from BaseInputConnection
+ 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;
+ }
+ }
+
+ // Implementation copy from BaseInputConnection
+ 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;
+ }
+ }
}
diff --git a/samples/game_text_input/game_text_input_testbed/app/src/main/java/com/gameinput/testbed/InputEnabledTextView.java b/samples/game_text_input/game_text_input_testbed/app/src/main/java/com/gameinput/testbed/InputEnabledTextView.java
index 9ec701d..561ea02 100644
--- a/samples/game_text_input/game_text_input_testbed/app/src/main/java/com/gameinput/testbed/InputEnabledTextView.java
+++ b/samples/game_text_input/game_text_input_testbed/app/src/main/java/com/gameinput/testbed/InputEnabledTextView.java
@@ -73,7 +73,10 @@
// Called when the IME has changed the input
@Override
public void stateChanged(State newState, boolean dismissed) {
- Log.d(LOG_TAG, "stateChanged: " + newState + " dismissed: " + dismissed);
+ Log.d(LOG_TAG, "stateChanged: " + newState.text
+ + ", s: " + newState.selectionStart + "-" + newState.selectionEnd
+ + ", cr: " + newState.composingRegionStart + "-" + newState.composingRegionEnd
+ );
onTextInputEventNative(newState);
}
diff --git a/samples/game_text_input/game_text_input_testbed/app/src/main/java/com/gameinput/testbed/MainActivity.java b/samples/game_text_input/game_text_input_testbed/app/src/main/java/com/gameinput/testbed/MainActivity.java
index 4d037bf..825d9eb 100644
--- a/samples/game_text_input/game_text_input_testbed/app/src/main/java/com/gameinput/testbed/MainActivity.java
+++ b/samples/game_text_input/game_text_input_testbed/app/src/main/java/com/gameinput/testbed/MainActivity.java
@@ -95,7 +95,6 @@
SpannableString str = new SpannableString(text);
if (selectionStart != selectionEnd) {
- Log.e("main", String.format("selection: %d to %d", selectionStart, selectionEnd));
str.setSpan(new BackgroundColorSpan(Color.YELLOW), selectionStart, selectionEnd, 0);
}