blob: 9d3281a34283e1580c100a0e28b694a93a8e3700 [file] [log] [blame]
/*
* Copyright (C) 2022 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.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED;
import static android.view.inputmethod.cts.util.InputMethodVisibilityVerifier.expectImeInvisible;
import static android.view.inputmethod.cts.util.InputMethodVisibilityVerifier.expectImeVisible;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.fail;
import android.app.Activity;
import android.app.Instrumentation;
import android.platform.test.annotations.AppModeSdkSandbox;
import android.view.WindowInsets;
import android.view.WindowInsetsController.OnControllableInsetsChangedListener;
import android.view.inputmethod.cts.util.EndToEndImeTestBase;
import android.view.inputmethod.cts.util.MetricsRecorder;
import android.view.inputmethod.cts.util.TestActivity;
import android.view.inputmethod.cts.util.TestUtils;
import android.view.inputmethod.nano.ImeProtoEnums;
import android.widget.EditText;
import android.widget.LinearLayout;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.compatibility.common.util.PollingCheck;
import com.android.cts.mockime.ImeSettings;
import com.android.cts.mockime.MockImeSession;
import com.android.os.nano.AtomsProto;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* Test suite to ensure IME stats get tracked and logged correctly.
*/
@RunWith(AndroidJUnit4.class)
@AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).")
public class InputMethodStatsTest extends EndToEndImeTestBase {
private static final String TAG = "InputMethodStatsTest";
private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(20);
private Instrumentation mInstrumentation;
/** The test app package name from which atoms will be logged. */
private String mPkgName;
@Before
public void setUp() throws Exception {
MetricsRecorder.removeConfig();
MetricsRecorder.clearReports();
mInstrumentation = InstrumentationRegistry.getInstrumentation();
mPkgName = mInstrumentation.getContext().getPackageName();
}
@After
public void tearDown() throws Exception {
MetricsRecorder.removeConfig();
MetricsRecorder.clearReports();
mInstrumentation = null;
mPkgName = "";
}
private TestActivity createTestActivity(final int windowFlags) {
return TestActivity.startSync(activity -> createLayout(windowFlags, activity));
}
private LinearLayout createLayout(final int windowFlags, final Activity activity) {
final var layout = new LinearLayout(activity);
layout.setOrientation(LinearLayout.VERTICAL);
final var editText = new EditText(activity);
editText.setText("Editable");
layout.addView(editText);
editText.requestFocus();
activity.getWindow().setSoftInputMode(windowFlags);
return layout;
}
/**
* Waits for the given inset type to be controllable on the given activity's
* {@link android.view.WindowInsetsController}.
*
* @implNote
* This is used to avoid the case where {@link android.view.InsetsController#show(int)}
* is called before IME insets control is available, starting a more complex flow which is
* currently harder to track with the {@link com.android.server.inputmethod.ImeTrackerService}
* system.
*
* TODO(b/263069667): Remove this method when the ImeInsetsSourceConsumer show flow is fixed.
*
* @param type the inset type waiting to be controllable.
* @param activity the activity whose Window Insets Controller to wait on.
*/
private void awaitControl(final int type, final Activity activity) {
final var latch = new CountDownLatch(1);
final OnControllableInsetsChangedListener listener = (controller, typeMask) -> {
if ((typeMask & type) != 0) {
latch.countDown();
}
};
TestUtils.runOnMainSync(() -> activity.getWindow()
.getDecorView()
.getWindowInsetsController()
.addOnControllableInsetsChangedListener(listener));
try {
if (!latch.await(TIMEOUT, TimeUnit.SECONDS)) {
fail("IME insets controls not available");
}
} catch (InterruptedException e) {
fail("Waiting for IMe insets controls to be available failed");
} finally {
TestUtils.runOnMainSync(() -> activity.getWindow()
.getDecorView()
.getWindowInsetsController()
.removeOnControllableInsetsChangedListener(listener));
}
}
/**
* Test the logging for a client show IME request.
*/
@Test
public void testClientShowImeRequestFinished() throws Throwable {
// Create mockImeSession to decouple from real IMEs,
// and enable calling expectImeVisible.
try (var imeSession = MockImeSession.create(
mInstrumentation.getContext(),
mInstrumentation.getUiAutomation(),
new ImeSettings.Builder())) {
// Wait for any outstanding IME requests to finish, to not interfere with test.
PollingCheck.waitFor(() -> !imeSession.hasPendingImeVisibilityRequests(),
"Test Setup Failed: There should be no pending IME requests present when the "
+ "test starts.");
MetricsRecorder.uploadConfigForPushedAtomWithUid(mPkgName,
AtomsProto.Atom.IME_REQUEST_FINISHED_FIELD_NUMBER,
false /* useUidAttributionChain */);
final var activity = createTestActivity(SOFT_INPUT_STATE_UNCHANGED);
awaitControl(WindowInsets.Type.ime(), activity);
expectImeInvisible(TIMEOUT);
TestUtils.runOnMainSync(() -> activity.getWindow()
.getDecorView()
.getWindowInsetsController()
.show(WindowInsets.Type.ime()));
expectImeVisible(TIMEOUT);
// Wait for any outstanding IME requests to finish, to capture all atoms successfully.
PollingCheck.waitFor(() -> !imeSession.hasPendingImeVisibilityRequests(),
"Test Error: Pending IME requests took too long, likely timing out.");
final var data = MetricsRecorder.getEventMetricDataList();
assertWithMessage("Number of atoms logged")
.that(data.size())
.isAtLeast(1);
try {
int successfulAtoms = 0;
for (int i = 0; i < data.size(); i++) {
final var atom = data.get(i).atom;
assertThat(atom).isNotNull();
final var imeRequestFinished = atom.getImeRequestFinished();
assertThat(imeRequestFinished).isNotNull();
// Skip cancelled requests.
if (imeRequestFinished.status == ImeProtoEnums.STATUS_CANCEL) continue;
successfulAtoms++;
assertWithMessage("Ime Request type")
.that(imeRequestFinished.type)
.isEqualTo(ImeProtoEnums.TYPE_SHOW);
assertWithMessage("Ime Request status")
.that(imeRequestFinished.status)
.isEqualTo(ImeProtoEnums.STATUS_SUCCESS);
assertWithMessage("Ime Request origin")
.that(imeRequestFinished.origin)
.isEqualTo(ImeProtoEnums.ORIGIN_CLIENT_SHOW_SOFT_INPUT);
}
assertWithMessage("Number of successful atoms logged")
.that(successfulAtoms)
.isAtLeast(1);
} catch (AssertionError e) {
throw new AssertionError(e.getMessage() + "\natoms data:\n" + data, e);
}
}
}
/**
* Test the logging for a client hide IME request.
*/
@Test
public void testClientHideImeRequestFinished() throws Exception {
// Create mockImeSession to decouple from real IMEs,
// and enable calling expectImeVisible and expectImeInvisible.
try (var imeSession = MockImeSession.create(
mInstrumentation.getContext(),
mInstrumentation.getUiAutomation(),
new ImeSettings.Builder())) {
// Wait for any outstanding IME requests to finish, to not interfere with test.
PollingCheck.waitFor(() -> !imeSession.hasPendingImeVisibilityRequests(),
"Test Setup Failed: There should be no pending IME requests present when the "
+ "test starts.");
MetricsRecorder.uploadConfigForPushedAtomWithUid(mPkgName,
AtomsProto.Atom.IME_REQUEST_FINISHED_FIELD_NUMBER,
false /* useUidAttributionChain */);
final var activity = createTestActivity(SOFT_INPUT_STATE_UNCHANGED);
awaitControl(WindowInsets.Type.ime(), activity);
expectImeInvisible(TIMEOUT);
TestUtils.runOnMainSync(() -> activity.getWindow()
.getDecorView()
.getWindowInsetsController()
.show(WindowInsets.Type.ime()));
expectImeVisible(TIMEOUT);
// Wait for any outstanding IME requests to finish, to capture all atoms successfully.
PollingCheck.waitFor(() -> !imeSession.hasPendingImeVisibilityRequests(),
"Test Error: Pending IME requests took too long, likely timing out.");
// Remove logs for the show requests.
MetricsRecorder.clearReports();
TestUtils.runOnMainSync(() -> activity.getWindow()
.getDecorView()
.getWindowInsetsController()
.hide(WindowInsets.Type.ime()));
expectImeInvisible(TIMEOUT);
// Wait for any outstanding IME requests to finish, to capture all atoms successfully.
PollingCheck.waitFor(() -> !imeSession.hasPendingImeVisibilityRequests(),
"Test Error: Pending IME requests took too long, likely timing out.");
final var data = MetricsRecorder.getEventMetricDataList();
assertWithMessage("Number of atoms logged")
.that(data.size())
.isAtLeast(1);
try {
int successfulAtoms = 0;
for (int i = 0; i < data.size(); i++) {
final var atom = data.get(i).atom;
assertThat(atom).isNotNull();
final var imeRequestFinished = atom.getImeRequestFinished();
assertThat(imeRequestFinished).isNotNull();
// Skip cancelled requests.
if (imeRequestFinished.status == ImeProtoEnums.STATUS_CANCEL) continue;
successfulAtoms++;
assertWithMessage("Ime Request type")
.that(imeRequestFinished.type)
.isEqualTo(ImeProtoEnums.TYPE_HIDE);
assertWithMessage("Ime Request status")
.that(imeRequestFinished.status)
.isEqualTo(ImeProtoEnums.STATUS_SUCCESS);
assertWithMessage("Ime Request origin")
.that(imeRequestFinished.origin)
.isEqualTo(ImeProtoEnums.ORIGIN_CLIENT_HIDE_SOFT_INPUT);
}
assertWithMessage("Number of successful atoms logged")
.that(successfulAtoms)
.isAtLeast(1);
} catch (AssertionError e) {
throw new AssertionError(e.getMessage() + "\natoms data:\n" + data, e);
}
}
}
/**
* Test the logging for a server show IME request.
*/
@Test
public void testServerShowImeRequestFinished() throws Exception {
// Create mockImeSession to decouple from real IMEs,
// and enable calling expectImeVisible and expectImeInvisible.
try (var imeSession = MockImeSession.create(
mInstrumentation.getContext(),
mInstrumentation.getUiAutomation(),
new ImeSettings.Builder())) {
// Wait for any outstanding IME requests to finish, to not interfere with test.
PollingCheck.waitFor(() -> !imeSession.hasPendingImeVisibilityRequests(),
"Test Setup Failed: There should be no pending IME requests present when the "
+ "test starts.");
MetricsRecorder.uploadConfigForPushedAtomWithUid(mPkgName,
AtomsProto.Atom.IME_REQUEST_FINISHED_FIELD_NUMBER,
false /* useUidAttributionChain */);
createTestActivity(SOFT_INPUT_STATE_ALWAYS_VISIBLE);
expectImeVisible(TIMEOUT);
// Wait for any outstanding IME requests to finish, to capture all atoms successfully.
PollingCheck.waitFor(() -> !imeSession.hasPendingImeVisibilityRequests(),
"Test Error: Pending IME requests took too long, likely timing out.");
final var data = MetricsRecorder.getEventMetricDataList();
assertWithMessage("Number of atoms logged")
.that(data.size())
.isAtLeast(1);
try {
int successfulAtoms = 0;
for (int i = 0; i < data.size(); i++) {
final var atom = data.get(i).atom;
assertThat(atom).isNotNull();
final var imeRequestFinished = atom.getImeRequestFinished();
assertThat(imeRequestFinished).isNotNull();
// Skip cancelled requests.
if (imeRequestFinished.status == ImeProtoEnums.STATUS_CANCEL) continue;
successfulAtoms++;
assertWithMessage("Ime Request type")
.that(imeRequestFinished.type)
.isEqualTo(ImeProtoEnums.TYPE_SHOW);
assertWithMessage("Ime Request status")
.that(imeRequestFinished.status)
.isEqualTo(ImeProtoEnums.STATUS_SUCCESS);
assertWithMessage("Ime Request origin")
.that(imeRequestFinished.origin)
.isEqualTo(ImeProtoEnums.ORIGIN_SERVER_START_INPUT);
}
assertWithMessage("Number of successful atoms logged")
.that(successfulAtoms)
.isAtLeast(1);
} catch (AssertionError e) {
throw new AssertionError(e.getMessage() + "\natoms data:\n" + data, e);
}
}
}
/**
* Test the logging for a server hide IME request.
*/
@Test
public void testServerHideImeRequestFinished() throws Exception {
// Create mockImeSession to decouple from real IMEs,
// and enable calling expectImeVisible and expectImeInvisible.
try (var imeSession = MockImeSession.create(
mInstrumentation.getContext(),
mInstrumentation.getUiAutomation(),
new ImeSettings.Builder())) {
// Wait for any outstanding IME requests to finish, to not interfere with test.
PollingCheck.waitFor(() -> !imeSession.hasPendingImeVisibilityRequests(),
"Test Setup Failed: There should be no pending IME requests present when the "
+ "test starts.");
MetricsRecorder.uploadConfigForPushedAtomWithUid(mPkgName,
AtomsProto.Atom.IME_REQUEST_FINISHED_FIELD_NUMBER,
false /* useUidAttributionChain */);
createTestActivity(SOFT_INPUT_STATE_ALWAYS_VISIBLE);
expectImeVisible(TIMEOUT);
// Wait for any outstanding IME requests to finish, to capture all atoms successfully.
PollingCheck.waitFor(() -> !imeSession.hasPendingImeVisibilityRequests(),
"Test Error: Pending IME requests took too long, likely timing out.");
// Remove logs for the show requests.
MetricsRecorder.clearReports();
// TODO: this is not actually an IME hide request from the server,
// but in the current configuration it is tracked like one.
// Will likely change in the future.
imeSession.callRequestHideSelf(0 /* flags */);
expectImeInvisible(TIMEOUT);
// Wait for any outstanding IME requests to finish, to capture all atoms successfully.
PollingCheck.waitFor(() -> !imeSession.hasPendingImeVisibilityRequests(),
"Test Error: Pending IME requests took too long, likely timing out.");
final var data = MetricsRecorder.getEventMetricDataList();
assertWithMessage("Number of atoms logged")
.that(data.size())
.isAtLeast(1);
try {
int successfulAtoms = 0;
for (int i = 0; i < data.size(); i++) {
final var atom = data.get(i).atom;
assertThat(atom).isNotNull();
final var imeRequestFinished = atom.getImeRequestFinished();
assertThat(imeRequestFinished).isNotNull();
// Skip cancelled requests.
if (imeRequestFinished.status == ImeProtoEnums.STATUS_CANCEL) continue;
successfulAtoms++;
assertWithMessage("Ime Request type")
.that(imeRequestFinished.type)
.isEqualTo(ImeProtoEnums.TYPE_HIDE);
assertWithMessage("Ime Request status")
.that(imeRequestFinished.status)
.isEqualTo(ImeProtoEnums.STATUS_SUCCESS);
assertWithMessage("Ime Request origin")
.that(imeRequestFinished.origin)
.isEqualTo(ImeProtoEnums.ORIGIN_SERVER_HIDE_INPUT);
}
assertWithMessage("Number of successful atoms logged")
.that(successfulAtoms)
.isAtLeast(1);
} catch (AssertionError e) {
throw new AssertionError(e.getMessage() + "\natoms data:\n" + data, e);
}
}
}
}