blob: 2da87eb85ab4c25429ac1f7bc3913d96c244e7e6 [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.textclassifier.cts;
import static android.content.pm.PackageManager.FEATURE_TOUCHSCREEN;
import static android.provider.Settings.Global.ANIMATOR_DURATION_SCALE;
import static android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE;
import static com.google.common.truth.Truth.assertThat;
import android.app.PendingIntent;
import android.app.RemoteAction;
import android.content.ContentResolver;
import android.content.Intent;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.view.textclassifier.TextClassification;
import android.view.textclassifier.TextClassifier;
import android.view.textclassifier.TextLinks;
import android.view.textclassifier.TextSelection;
import android.widget.TextView;
import androidx.core.os.BuildCompat;
import androidx.test.core.app.ActivityScenario;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.filters.FlakyTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.BySelector;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject2;
import com.android.compatibility.common.util.ApiTest;
import com.android.compatibility.common.util.ShellUtils;
import com.android.compatibility.common.util.SystemUtil;
import com.android.compatibility.common.util.Timeout;
import org.junit.AfterClass;
import org.junit.Assume;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import java.util.Collections;
public class TextViewIntegrationTest {
private static final String LOG_TAG = "TextViewIntegrationTest";
private static final String TOOLBAR_ITEM_LABEL = "TB@#%!";
private static final Timeout UI_TIMEOUT = new Timeout("UI_TIMEOUT", 2_000, 2F, 10_000);
private SimpleTextClassifier mSimpleTextClassifier;
@Rule
public ActivityScenarioRule<TextViewActivity> rule = new ActivityScenarioRule<>(
TextViewActivity.class);
private static float sOriginalAnimationDurationScale;
private static float sOriginalTransitionAnimationDurationScale;
private static final UiDevice sDevice =
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
@Before
public void setup() throws Exception {
Assume.assumeTrue(
ApplicationProvider.getApplicationContext().getPackageManager()
.hasSystemFeature(FEATURE_TOUCHSCREEN));
mSimpleTextClassifier = new SimpleTextClassifier();
sDevice.wakeUp();
dismissKeyguard();
closeSystemDialog();
}
private void dismissKeyguard() {
ShellUtils.runShellCommand("wm dismiss-keyguard");
}
private static void closeSystemDialog() {
ShellUtils.runShellCommand("am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS");
}
private static void dumpScreenInformation(String testName) {
// Dump window layer state
String result = ShellUtils.runShellCommand("dumpsys window windows");
Helper.dumpsysAndSave(result, testName, Helper.LOCAL_TEST_FILES_DIR);
// Take screenshot
Helper.takeScreenshotAndSave(ApplicationProvider.getApplicationContext(),
testName, Helper.LOCAL_TEST_FILES_DIR);
}
@BeforeClass
public static void disableAnimation() {
SystemUtil.runWithShellPermissionIdentity(() -> {
ContentResolver resolver =
ApplicationProvider.getApplicationContext().getContentResolver();
sOriginalAnimationDurationScale =
Settings.Global.getFloat(resolver, ANIMATOR_DURATION_SCALE, 1f);
Settings.Global.putFloat(resolver, ANIMATOR_DURATION_SCALE, 0);
sOriginalTransitionAnimationDurationScale =
Settings.Global.getFloat(resolver, TRANSITION_ANIMATION_SCALE, 1f);
Settings.Global.putFloat(resolver, TRANSITION_ANIMATION_SCALE, 0);
});
}
@AfterClass
public static void restoreAnimation() {
SystemUtil.runWithShellPermissionIdentity(() -> {
Settings.Global.putFloat(
ApplicationProvider.getApplicationContext().getContentResolver(),
ANIMATOR_DURATION_SCALE, sOriginalAnimationDurationScale);
Settings.Global.putFloat(
ApplicationProvider.getApplicationContext().getContentResolver(),
TRANSITION_ANIMATION_SCALE, sOriginalTransitionAnimationDurationScale);
});
}
@Test
@FlakyTest
public void smartLinkify() throws Exception {
ActivityScenario<TextViewActivity> scenario = rule.getScenario();
// Linkify the text.
final String TEXT = "Link: https://www.android.com";
Spannable linkifiedText = createLinkifiedText(TEXT);
scenario.onActivity(activity -> {
TextView textView = activity.findViewById(R.id.textview);
textView.setText(linkifiedText);
textView.setTextClassifier(mSimpleTextClassifier);
textView.setMovementMethod(LinkMovementMethod.getInstance());
TextLinks.TextLinkSpan[] spans = linkifiedText.getSpans(0, TEXT.length(),
TextLinks.TextLinkSpan.class);
assertThat(spans).hasLength(1);
});
// To wait for the rendering of the activity to be completed, so that the upcoming click
// action will work.
Thread.sleep(2000);
try {
UiObject2 textview = waitForObject(By.text(linkifiedText.toString()));
textview.click();
assertFloatingToolbarIsDisplayed();
} catch (Throwable t) {
dumpScreenInformation("smartLinkify");
throw t;
}
}
@Test
public void smartSelection_suggestSelectionNotIncludeTextClassification() throws Exception {
Assume.assumeTrue(BuildCompat.isAtLeastS());
smartSelectionInternal("smartSelection_suggestSelectionNotIncludeTextClassification");
assertThat(mSimpleTextClassifier.getClassifyTextInvocationCount()).isEqualTo(1);
}
@Test
public void smartSelection_suggestSelectionIncludeTextClassification() throws Exception {
Assume.assumeTrue(isAtLeastS());
mSimpleTextClassifier.setIncludeTextClassification(true);
smartSelectionInternal("smartSelection_suggestSelectionIncludeTextClassification");
assertThat(mSimpleTextClassifier.getClassifyTextInvocationCount()).isEqualTo(0);
}
@Test
@Ignore // Enable the test once b/187862341 is fixed.
public void smartSelection_cancelSelectionDoesNotInvokeClassifyText() throws Exception {
Assume.assumeTrue(isAtLeastS());
smartSelectionInternal("smartSelection_cancelSelectionDoesNotInvokeClassifyText");
final String text = "Link: https://www.android.com";
UiObject2 textview = waitForObject(By.text(text));
textview.click();
Thread.sleep(1000);
assertThat(mSimpleTextClassifier.getClassifyTextInvocationCount()).isEqualTo(1);
}
// TODO: re-use now. Refactor to have a folder/test class for toolbar
@Test
@ApiTest(apis = "android.view.View#startActionMode")
public void smartSelection_toolbarContainerNoContentDescription() throws Exception {
smartSelectionInternal("smartSelection_toolbarContainerNoContentDescription");
UiObject2 toolbarContainer =
sDevice.findObject(By.res("android", "floating_popup_container"));
assertThat(toolbarContainer).isNotNull();
assertThat(toolbarContainer.getContentDescription()).isNull();
}
private void smartSelectionInternal(String testName) throws Exception {
ActivityScenario<TextViewActivity> scenario = rule.getScenario();
final String TEXT = "Link: https://www.android.com";
scenario.onActivity(activity -> {
TextView textView = activity.findViewById(R.id.textview);
textView.setTextIsSelectable(true);
textView.setText(TEXT);
textView.setTextClassifier(mSimpleTextClassifier);
});
// Long press the url to perform smart selection.
try {
UiObject2 textview = waitForObject(By.text(TEXT));
textview.click(3_000);
assertFloatingToolbarIsDisplayed();
} catch (Throwable t) {
dumpScreenInformation(testName);
throw t;
}
}
private boolean isAtLeastS() {
return Build.VERSION.SDK_INT >= 31;
}
private Spannable createLinkifiedText(CharSequence text) {
TextLinks.Request request = new TextLinks.Request.Builder(text)
.setEntityConfig(
new TextClassifier.EntityConfig.Builder()
.setIncludedTypes(Collections.singleton(TextClassifier.TYPE_URL))
.build())
.build();
TextLinks textLinks = mSimpleTextClassifier.generateLinks(request);
Spannable linkifiedText = new SpannableString(text);
int resultCode = textLinks.apply(
linkifiedText,
TextLinks.APPLY_STRATEGY_REPLACE,
/* spanFactory= */null);
assertThat(resultCode).isEqualTo(TextLinks.STATUS_LINKS_APPLIED);
return linkifiedText;
}
private static void assertFloatingToolbarIsDisplayed() throws Exception {
// Simply check that the toolbar item is visible.
UiObject2 toolbarObject = waitForObject(By.text(TOOLBAR_ITEM_LABEL));
assertThat(toolbarObject).isNotNull();
}
private static UiObject2 waitForObject(BySelector selector) throws Exception {
return UI_TIMEOUT.run("waitForObject(" + selector + ")",
() -> sDevice.findObject(selector));
}
/**
* A {@link TextClassifier} that can only annotate the android.com url. Do not reuse the same
* instance across tests.
*/
private static class SimpleTextClassifier implements TextClassifier {
private static final String ANDROID_URL = "https://www.android.com";
private static final Icon NO_ICON = Icon.createWithData(new byte[0], 0, 0);
private boolean mSetIncludeTextClassification = false;
private int mClassifyTextInvocationCount = 0;
public void setIncludeTextClassification(boolean setIncludeTextClassification) {
mSetIncludeTextClassification = setIncludeTextClassification;
}
public int getClassifyTextInvocationCount() {
return mClassifyTextInvocationCount;
}
@Override
public TextSelection suggestSelection(TextSelection.Request request) {
int start = request.getText().toString().indexOf(ANDROID_URL);
if (start == -1) {
return new TextSelection.Builder(
request.getStartIndex(), request.getEndIndex())
.build();
}
TextSelection.Builder builder =
new TextSelection.Builder(start, start + ANDROID_URL.length())
.setEntityType(TextClassifier.TYPE_URL, 1.0f);
if (mSetIncludeTextClassification) {
builder.setTextClassification(createAndroidUrlTextClassification());
}
return builder.build();
}
@Override
public TextClassification classifyText(TextClassification.Request request) {
mClassifyTextInvocationCount += 1;
String spanText = request.getText().toString()
.substring(request.getStartIndex(), request.getEndIndex());
if (TextUtils.equals(ANDROID_URL, spanText)) {
return createAndroidUrlTextClassification();
}
return new TextClassification.Builder().build();
}
private TextClassification createAndroidUrlTextClassification() {
TextClassification.Builder builder =
new TextClassification.Builder().setText(ANDROID_URL);
builder.setEntityType(TextClassifier.TYPE_URL, 1.0f);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(ANDROID_URL));
PendingIntent pendingIntent = PendingIntent.getActivity(
ApplicationProvider.getApplicationContext(),
/* requestCode= */ 0,
intent,
PendingIntent.FLAG_IMMUTABLE);
RemoteAction remoteAction =
new RemoteAction(NO_ICON, TOOLBAR_ITEM_LABEL, "cont-descr", pendingIntent);
remoteAction.setShouldShowIcon(false);
builder.addAction(remoteAction);
return builder.build();
}
@Override
public TextLinks generateLinks(TextLinks.Request request) {
Log.d("TextViewIntegrationTest", "generateLinks for " + request.getText().toString());
TextLinks.Builder builder = new TextLinks.Builder(request.getText().toString());
int index = request.getText().toString().indexOf(ANDROID_URL);
if (index == -1) {
return builder.build();
}
builder.addLink(index,
index + ANDROID_URL.length(),
Collections.singletonMap(TextClassifier.TYPE_URL, 1.0f));
return builder.build();
}
}
}