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