• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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