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