1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.view.textclassifier.cts; 18 19 import static android.content.pm.PackageManager.FEATURE_TOUCHSCREEN; 20 import static android.provider.Settings.Global.ANIMATOR_DURATION_SCALE; 21 import static android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE; 22 23 import static com.google.common.truth.Truth.assertThat; 24 25 import android.app.PendingIntent; 26 import android.app.RemoteAction; 27 import android.content.ContentResolver; 28 import android.content.Intent; 29 import android.graphics.drawable.Icon; 30 import android.net.Uri; 31 import android.os.Build; 32 import android.provider.Settings; 33 import android.text.Spannable; 34 import android.text.SpannableString; 35 import android.text.TextUtils; 36 import android.text.method.LinkMovementMethod; 37 import android.view.textclassifier.TextClassification; 38 import android.view.textclassifier.TextClassifier; 39 import android.view.textclassifier.TextLinks; 40 import android.view.textclassifier.TextSelection; 41 import android.widget.TextView; 42 43 import androidx.core.os.BuildCompat; 44 import androidx.test.core.app.ActivityScenario; 45 import androidx.test.core.app.ApplicationProvider; 46 import androidx.test.ext.junit.rules.ActivityScenarioRule; 47 import androidx.test.platform.app.InstrumentationRegistry; 48 import androidx.test.uiautomator.By; 49 import androidx.test.uiautomator.BySelector; 50 import androidx.test.uiautomator.UiDevice; 51 import androidx.test.uiautomator.UiObject2; 52 53 import com.android.compatibility.common.util.ApiTest; 54 import com.android.compatibility.common.util.ShellUtils; 55 import com.android.compatibility.common.util.SystemUtil; 56 import com.android.compatibility.common.util.Timeout; 57 58 import org.junit.AfterClass; 59 import org.junit.Assume; 60 import org.junit.Before; 61 import org.junit.BeforeClass; 62 import org.junit.Ignore; 63 import org.junit.Rule; 64 import org.junit.Test; 65 66 import java.util.Collections; 67 68 public class TextViewIntegrationTest { 69 private static final String LOG_TAG = "TextViewIntegrationTest"; 70 private static final String TOOLBAR_ITEM_LABEL = "TB@#%!"; 71 72 private static final Timeout UI_TIMEOUT = new Timeout("UI_TIMEOUT", 2_000, 2F, 10_000); 73 74 private SimpleTextClassifier mSimpleTextClassifier; 75 76 @Rule 77 public ActivityScenarioRule<TextViewActivity> rule = new ActivityScenarioRule<>( 78 TextViewActivity.class); 79 80 private static float sOriginalAnimationDurationScale; 81 private static float sOriginalTransitionAnimationDurationScale; 82 83 private static final UiDevice sDevice = 84 UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); 85 86 @Before setup()87 public void setup() throws Exception { 88 Assume.assumeTrue( 89 ApplicationProvider.getApplicationContext().getPackageManager() 90 .hasSystemFeature(FEATURE_TOUCHSCREEN)); 91 mSimpleTextClassifier = new SimpleTextClassifier(); 92 sDevice.wakeUp(); 93 dismissKeyguard(); 94 closeSystemDialog(); 95 } 96 dismissKeyguard()97 private void dismissKeyguard() { 98 ShellUtils.runShellCommand("wm dismiss-keyguard"); 99 } 100 closeSystemDialog()101 private static void closeSystemDialog() { 102 ShellUtils.runShellCommand("am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS"); 103 } 104 dumpScreenInformation(String testName)105 private static void dumpScreenInformation(String testName) { 106 // Dump window layer state 107 String result = ShellUtils.runShellCommand("dumpsys window windows"); 108 Helper.dumpsysAndSave(result, testName, Helper.LOCAL_TEST_FILES_DIR); 109 // Take screenshot 110 Helper.takeScreenshotAndSave(ApplicationProvider.getApplicationContext(), 111 testName, Helper.LOCAL_TEST_FILES_DIR); 112 } 113 114 @BeforeClass disableAnimation()115 public static void disableAnimation() { 116 SystemUtil.runWithShellPermissionIdentity(() -> { 117 ContentResolver resolver = 118 ApplicationProvider.getApplicationContext().getContentResolver(); 119 sOriginalAnimationDurationScale = 120 Settings.Global.getFloat(resolver, ANIMATOR_DURATION_SCALE, 1f); 121 Settings.Global.putFloat(resolver, ANIMATOR_DURATION_SCALE, 0); 122 123 sOriginalTransitionAnimationDurationScale = 124 Settings.Global.getFloat(resolver, TRANSITION_ANIMATION_SCALE, 1f); 125 Settings.Global.putFloat(resolver, TRANSITION_ANIMATION_SCALE, 0); 126 }); 127 } 128 129 @AfterClass restoreAnimation()130 public static void restoreAnimation() { 131 SystemUtil.runWithShellPermissionIdentity(() -> { 132 Settings.Global.putFloat( 133 ApplicationProvider.getApplicationContext().getContentResolver(), 134 ANIMATOR_DURATION_SCALE, sOriginalAnimationDurationScale); 135 136 Settings.Global.putFloat( 137 ApplicationProvider.getApplicationContext().getContentResolver(), 138 TRANSITION_ANIMATION_SCALE, sOriginalTransitionAnimationDurationScale); 139 }); 140 } 141 142 @Test smartLinkify()143 public void smartLinkify() throws Exception { 144 ActivityScenario<TextViewActivity> scenario = rule.getScenario(); 145 // Linkify the text. 146 final String TEXT = "Link: https://www.android.com"; 147 Spannable linkifiedText = createLinkifiedText(TEXT); 148 scenario.onActivity(activity -> { 149 TextView textView = activity.findViewById(R.id.textview); 150 textView.setText(linkifiedText); 151 textView.setTextClassifier(mSimpleTextClassifier); 152 textView.setMovementMethod(LinkMovementMethod.getInstance()); 153 TextLinks.TextLinkSpan[] spans = linkifiedText.getSpans(0, TEXT.length(), 154 TextLinks.TextLinkSpan.class); 155 assertThat(spans).hasLength(1); 156 }); 157 // To wait for the rendering of the activity to be completed, so that the upcoming click 158 // action will work. 159 Thread.sleep(2000); 160 try { 161 UiObject2 textview = waitForObject(By.text(linkifiedText.toString())); 162 textview.click(); 163 164 assertFloatingToolbarIsDisplayed(); 165 } catch (Throwable t) { 166 dumpScreenInformation("smartLinkify"); 167 throw t; 168 } 169 } 170 171 @Test smartSelection_suggestSelectionNotIncludeTextClassification()172 public void smartSelection_suggestSelectionNotIncludeTextClassification() throws Exception { 173 Assume.assumeTrue(BuildCompat.isAtLeastS()); 174 smartSelectionInternal("smartSelection_suggestSelectionNotIncludeTextClassification"); 175 176 assertThat(mSimpleTextClassifier.getClassifyTextInvocationCount()).isEqualTo(1); 177 } 178 179 @Test smartSelection_suggestSelectionIncludeTextClassification()180 public void smartSelection_suggestSelectionIncludeTextClassification() throws Exception { 181 Assume.assumeTrue(isAtLeastS()); 182 mSimpleTextClassifier.setIncludeTextClassification(true); 183 smartSelectionInternal("smartSelection_suggestSelectionIncludeTextClassification"); 184 185 assertThat(mSimpleTextClassifier.getClassifyTextInvocationCount()).isEqualTo(0); 186 } 187 188 @Test 189 @Ignore // Enable the test once b/187862341 is fixed. smartSelection_cancelSelectionDoesNotInvokeClassifyText()190 public void smartSelection_cancelSelectionDoesNotInvokeClassifyText() throws Exception { 191 Assume.assumeTrue(isAtLeastS()); 192 smartSelectionInternal("smartSelection_cancelSelectionDoesNotInvokeClassifyText"); 193 final String text = "Link: https://www.android.com"; 194 UiObject2 textview = waitForObject(By.text(text)); 195 textview.click(); 196 197 Thread.sleep(1000); 198 199 assertThat(mSimpleTextClassifier.getClassifyTextInvocationCount()).isEqualTo(1); 200 } 201 202 // TODO: re-use now. Refactor to have a folder/test class for toolbar 203 @Test 204 @ApiTest(apis = "android.view.View#startActionMode") smartSelection_toolbarContainerNoContentDescription()205 public void smartSelection_toolbarContainerNoContentDescription() throws Exception { 206 smartSelectionInternal("smartSelection_toolbarContainerNoContentDescription"); 207 208 UiObject2 toolbarContainer = 209 sDevice.findObject(By.res("android", "floating_popup_container")); 210 assertThat(toolbarContainer).isNotNull(); 211 assertThat(toolbarContainer.getContentDescription()).isNull(); 212 } 213 smartSelectionInternal(String testName)214 private void smartSelectionInternal(String testName) throws Exception { 215 ActivityScenario<TextViewActivity> scenario = rule.getScenario(); 216 final String TEXT = "Link: https://www.android.com"; 217 scenario.onActivity(activity -> { 218 TextView textView = activity.findViewById(R.id.textview); 219 textView.setTextIsSelectable(true); 220 textView.setText(TEXT); 221 textView.setTextClassifier(mSimpleTextClassifier); 222 }); 223 // Long press the url to perform smart selection. 224 try { 225 UiObject2 textview = waitForObject(By.text(TEXT)); 226 textview.click(3_000); 227 228 assertFloatingToolbarIsDisplayed(); 229 } catch (Throwable t) { 230 dumpScreenInformation(testName); 231 throw t; 232 } 233 } 234 isAtLeastS()235 private boolean isAtLeastS() { 236 return Build.VERSION.SDK_INT >= 31; 237 } 238 createLinkifiedText(CharSequence text)239 private Spannable createLinkifiedText(CharSequence text) { 240 TextLinks.Request request = new TextLinks.Request.Builder(text) 241 .setEntityConfig( 242 new TextClassifier.EntityConfig.Builder() 243 .setIncludedTypes(Collections.singleton(TextClassifier.TYPE_URL)) 244 .build()) 245 .build(); 246 TextLinks textLinks = mSimpleTextClassifier.generateLinks(request); 247 Spannable linkifiedText = new SpannableString(text); 248 int resultCode = textLinks.apply( 249 linkifiedText, 250 TextLinks.APPLY_STRATEGY_REPLACE, 251 /* spanFactory= */null); 252 assertThat(resultCode).isEqualTo(TextLinks.STATUS_LINKS_APPLIED); 253 return linkifiedText; 254 } 255 assertFloatingToolbarIsDisplayed()256 private static void assertFloatingToolbarIsDisplayed() throws Exception { 257 // Simply check that the toolbar item is visible. 258 UiObject2 toolbarObject = waitForObject(By.text(TOOLBAR_ITEM_LABEL)); 259 assertThat(toolbarObject).isNotNull(); 260 } 261 waitForObject(BySelector selector)262 private static UiObject2 waitForObject(BySelector selector) throws Exception { 263 return UI_TIMEOUT.run("waitForObject(" + selector + ")", 264 () -> sDevice.findObject(selector)); 265 } 266 267 /** 268 * A {@link TextClassifier} that can only annotate the android.com url. Do not reuse the same 269 * instance across tests. 270 */ 271 private static class SimpleTextClassifier implements TextClassifier { 272 private static final String ANDROID_URL = "https://www.android.com"; 273 private static final Icon NO_ICON = Icon.createWithData(new byte[0], 0, 0); 274 private boolean mSetIncludeTextClassification = false; 275 private int mClassifyTextInvocationCount = 0; 276 setIncludeTextClassification(boolean setIncludeTextClassification)277 public void setIncludeTextClassification(boolean setIncludeTextClassification) { 278 mSetIncludeTextClassification = setIncludeTextClassification; 279 } 280 getClassifyTextInvocationCount()281 public int getClassifyTextInvocationCount() { 282 return mClassifyTextInvocationCount; 283 } 284 285 @Override suggestSelection(TextSelection.Request request)286 public TextSelection suggestSelection(TextSelection.Request request) { 287 int start = request.getText().toString().indexOf(ANDROID_URL); 288 if (start == -1) { 289 return new TextSelection.Builder( 290 request.getStartIndex(), request.getEndIndex()) 291 .build(); 292 } 293 TextSelection.Builder builder = 294 new TextSelection.Builder(start, start + ANDROID_URL.length()) 295 .setEntityType(TextClassifier.TYPE_URL, 1.0f); 296 if (mSetIncludeTextClassification) { 297 builder.setTextClassification(createAndroidUrlTextClassification()); 298 } 299 return builder.build(); 300 } 301 302 @Override classifyText(TextClassification.Request request)303 public TextClassification classifyText(TextClassification.Request request) { 304 mClassifyTextInvocationCount += 1; 305 String spanText = request.getText().toString() 306 .substring(request.getStartIndex(), request.getEndIndex()); 307 if (TextUtils.equals(ANDROID_URL, spanText)) { 308 return createAndroidUrlTextClassification(); 309 } 310 return new TextClassification.Builder().build(); 311 } 312 createAndroidUrlTextClassification()313 private TextClassification createAndroidUrlTextClassification() { 314 TextClassification.Builder builder = 315 new TextClassification.Builder().setText(ANDROID_URL); 316 builder.setEntityType(TextClassifier.TYPE_URL, 1.0f); 317 318 Intent intent = new Intent(Intent.ACTION_VIEW); 319 intent.setData(Uri.parse(ANDROID_URL)); 320 PendingIntent pendingIntent = PendingIntent.getActivity( 321 ApplicationProvider.getApplicationContext(), 322 /* requestCode= */ 0, 323 intent, 324 PendingIntent.FLAG_IMMUTABLE); 325 326 RemoteAction remoteAction = 327 new RemoteAction(NO_ICON, TOOLBAR_ITEM_LABEL, "cont-descr", pendingIntent); 328 remoteAction.setShouldShowIcon(false); 329 builder.addAction(remoteAction); 330 return builder.build(); 331 } 332 333 @Override generateLinks(TextLinks.Request request)334 public TextLinks generateLinks(TextLinks.Request request) { 335 TextLinks.Builder builder = new TextLinks.Builder(request.getText().toString()); 336 int index = request.getText().toString().indexOf(ANDROID_URL); 337 if (index == -1) { 338 return builder.build(); 339 } 340 builder.addLink(index, 341 index + ANDROID_URL.length(), 342 Collections.singletonMap(TextClassifier.TYPE_URL, 1.0f)); 343 return builder.build(); 344 } 345 } 346 } 347