blob: 4eb65735f985b3a34a0933b3734c181b2c8536c0 [file] [log] [blame]
/*
* Copyright (C) 2020 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.view.inputmethod.cts;
import static android.inputmethodservice.InputMethodService.FINISH_INPUT_NO_FALLBACK_CONNECTION;
import static android.view.View.SCREEN_STATE_OFF;
import static android.view.View.SCREEN_STATE_ON;
import static android.view.View.VISIBLE;
import static com.android.cts.mockime.ImeEventStreamTestUtils.editorMatcher;
import static com.android.cts.mockime.ImeEventStreamTestUtils.expectBindInput;
import static com.android.cts.mockime.ImeEventStreamTestUtils.expectCommand;
import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent;
import static com.android.cts.mockime.ImeEventStreamTestUtils.notExpectEvent;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import android.app.Instrumentation;
import android.content.Context;
import android.content.pm.PackageManager;
import android.inputmethodservice.InputMethodService;
import android.os.IBinder;
import android.os.Process;
import android.os.SystemClock;
import android.platform.test.annotations.AppModeFull;
import android.text.TextUtils;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.cts.util.DisableScreenDozeRule;
import android.view.inputmethod.cts.util.EndToEndImeTestBase;
import android.view.inputmethod.cts.util.RequireImeCompatFlagRule;
import android.view.inputmethod.cts.util.TestActivity;
import android.view.inputmethod.cts.util.TestUtils;
import android.view.inputmethod.cts.util.UnlockScreenRule;
import android.view.inputmethod.cts.util.WindowFocusStealer;
import android.widget.EditText;
import android.widget.LinearLayout;
import androidx.test.filters.MediumTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.CtsTouchUtils;
import com.android.cts.mockime.ImeCommand;
import com.android.cts.mockime.ImeEvent;
import com.android.cts.mockime.ImeEventStream;
import com.android.cts.mockime.ImeSettings;
import com.android.cts.mockime.MockImeSession;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
@MediumTest
@RunWith(AndroidJUnit4.class)
public class InputMethodStartInputLifecycleTest extends EndToEndImeTestBase {
@Rule
public final DisableScreenDozeRule mDisableScreenDozeRule = new DisableScreenDozeRule();
@Rule
public final UnlockScreenRule mUnlockScreenRule = new UnlockScreenRule();
@Rule
public final RequireImeCompatFlagRule mRequireImeCompatFlagRule = new RequireImeCompatFlagRule(
FINISH_INPUT_NO_FALLBACK_CONNECTION, true);
private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5);
@AppModeFull(reason = "KeyguardManager is not accessible from instant apps")
@Test
public void testInputConnectionStateWhenScreenStateChanges() throws Exception {
final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
final Context context = instrumentation.getTargetContext();
final InputMethodManager imm = context.getSystemService(InputMethodManager.class);
assumeTrue(context.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_INPUT_METHODS));
final AtomicReference<EditText> focusedEditTextRef = new AtomicReference<>();
try (MockImeSession imeSession = MockImeSession.create(
context, instrumentation.getUiAutomation(), new ImeSettings.Builder())) {
final ImeEventStream stream = imeSession.openEventStream();
final String marker = InputMethodManagerTest.class.getName() + "/"
+ SystemClock.elapsedRealtimeNanos();
final AtomicInteger screenStateCallbackRef = new AtomicInteger(-1);
TestActivity.startSync(activity -> {
final LinearLayout layout = new LinearLayout(activity);
layout.setOrientation(LinearLayout.VERTICAL);
final EditText focusedEditText = new EditText(activity) {
@Override
public void onScreenStateChanged(int screenState) {
super.onScreenStateChanged(screenState);
screenStateCallbackRef.set(screenState);
}
};
focusedEditText.setPrivateImeOptions(marker);
focusedEditText.setHint("editText");
layout.addView(focusedEditText);
focusedEditText.requestFocus();
focusedEditTextRef.set(focusedEditText);
final EditText nonFocusedEditText = new EditText(activity);
layout.addView(nonFocusedEditText);
return layout;
});
// Expected onStartInput when TestActivity launched.
final EditText editText = focusedEditTextRef.get();
expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
// Expected text commit will not work when turnScreenOff.
TestUtils.turnScreenOff();
TestUtils.waitOnMainUntil(() -> screenStateCallbackRef.get() == SCREEN_STATE_OFF
&& editText.getWindowVisibility() != VISIBLE, TIMEOUT);
if (MockImeSession.isFinishInputNoFallbackConnectionEnabled()) {
// Expected only onFinishInput and the EditText is inactive for input method.
expectEvent(stream, onFinishInputMatcher(), TIMEOUT);
notExpectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
assertFalse(TestUtils.getOnMainSync(() -> imm.isActive(editText)));
assertFalse(TestUtils.getOnMainSync(() -> imm.isAcceptingText()));
} else {
expectEvent(stream, onFinishInputMatcher(), TIMEOUT);
}
final ImeCommand commit = imeSession.callCommitText("Hi!", 1);
expectCommand(stream, commit, TIMEOUT);
TestUtils.waitOnMainUntil(() -> !TextUtils.equals(editText.getText(), "Hi!"), TIMEOUT,
"InputMethodService#commitText should not work after screen off");
// Expected text commit will work when turnScreenOn.
TestUtils.turnScreenOn();
TestUtils.unlockScreen();
TestUtils.waitOnMainUntil(() -> screenStateCallbackRef.get() == SCREEN_STATE_ON
&& editText.getWindowVisibility() == VISIBLE, TIMEOUT);
CtsTouchUtils.emulateTapOnViewCenter(instrumentation, null, editText);
expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
if (MockImeSession.isFinishInputNoFallbackConnectionEnabled()) {
// Expected only onStartInput and the EditText is active for input method.
notExpectEvent(stream, onFinishInputMatcher(), TIMEOUT);
}
assertTrue(TestUtils.getOnMainSync(
() -> imm.isActive(editText) && imm.isAcceptingText()));
final ImeCommand commit1 = imeSession.callCommitText("Hello!", 1);
expectCommand(stream, commit1, TIMEOUT);
TestUtils.waitOnMainUntil(() -> TextUtils.equals(editText.getText(), "Hello!"), TIMEOUT,
"InputMethodService#commitText should work after screen on");
}
}
/**
* Test case for Bug 158624922 and Bug 152373385.
*
* Test {@link android.inputmethodservice.InputMethodService#onStartInput(EditorInfo, boolean)}
* and {@link InputMethodService#onFinishInput()} won't be called and the input connection
* remains active, even when a non-IME focusable window hosted by a different process
* temporarily becomes the focused window.
*/
@Test
public void testNoStartNewInputWhileOtherProcessHasWindowFocus() throws Exception {
final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
try (MockImeSession imeSession = MockImeSession.create(
instrumentation.getContext(),
instrumentation.getUiAutomation(),
new ImeSettings.Builder())) {
final ImeEventStream stream = imeSession.openEventStream();
final String marker = InputMethodStartInputLifecycleTest.class.getName() + "/"
+ SystemClock.elapsedRealtimeNanos();
final EditText editText = launchTestActivity(marker);
TestUtils.runOnMainSync(() -> editText.requestFocus());
// Wait until the MockIme gets bound to the TestActivity.
expectBindInput(stream, Process.myPid(), TIMEOUT);
expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
// Get app window token
final IBinder appWindowToken = TestUtils.getOnMainSync(
() -> editText.getApplicationWindowToken());
try (WindowFocusStealer focusStealer =
WindowFocusStealer.connect(instrumentation.getTargetContext(), TIMEOUT)) {
focusStealer.stealWindowFocus(appWindowToken, TIMEOUT);
// Wait until the edit text loses window focus.
TestUtils.waitOnMainUntil(() -> !editText.hasWindowFocus(), TIMEOUT);
}
// Wait until the edit text gains window focus again.
TestUtils.waitOnMainUntil(() -> editText.hasWindowFocus(), TIMEOUT);
// Not expect the input connection will be started or finished even gaining non-IME
// focusable window focus.
notExpectEvent(stream, event -> "onFinishInput".equals(event.getEventName())
|| "onStartInput".equals(event.getEventName()), TIMEOUT);
// Verify the input connection of the EditText is still active and can accept text.
final InputMethodManager imm = editText.getContext().getSystemService(
InputMethodManager.class);
assertTrue(TestUtils.getOnMainSync(() -> imm.isActive(editText)));
assertTrue(TestUtils.getOnMainSync(() -> imm.isAcceptingText()));
}
}
private EditText launchTestActivity(String marker) {
final AtomicReference<EditText> editTextRef = new AtomicReference<>();
TestActivity.startSync(activity-> {
final LinearLayout layout = new LinearLayout(activity);
layout.setOrientation(LinearLayout.VERTICAL);
final EditText editText = new EditText(activity);
editText.setPrivateImeOptions(marker);
editText.setHint("editText");
editText.requestFocus();
editTextRef.set(editText);
layout.addView(editText);
return layout;
});
return editTextRef.get();
}
private static Predicate<ImeEvent> onFinishInputMatcher() {
return event -> TextUtils.equals("onFinishInput", event.getEventName());
}
}