1 /*
2  * Copyright (C) 2019 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 package android.view.textclassifier.cts;
17 
18 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
19 
20 import static com.google.common.truth.Truth.assertThat;
21 
22 import static org.junit.Assume.assumeTrue;
23 
24 import android.app.PendingIntent;
25 import android.content.ComponentName;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.graphics.drawable.Icon;
29 import android.os.CancellationSignal;
30 import android.os.SystemClock;
31 import android.service.textclassifier.TextClassifierService;
32 import android.view.textclassifier.ConversationActions;
33 import android.view.textclassifier.SelectionEvent;
34 import android.view.textclassifier.TextClassification;
35 import android.view.textclassifier.TextClassificationContext;
36 import android.view.textclassifier.TextClassificationManager;
37 import android.view.textclassifier.TextClassificationSessionId;
38 import android.view.textclassifier.TextClassifier;
39 import android.view.textclassifier.TextSelection;
40 
41 import androidx.core.os.BuildCompat;
42 import androidx.test.InstrumentationRegistry;
43 import androidx.test.core.app.ApplicationProvider;
44 import androidx.test.runner.AndroidJUnit4;
45 
46 import com.android.compatibility.common.util.BlockingBroadcastReceiver;
47 import com.android.compatibility.common.util.RequiredServiceRule;
48 import com.android.compatibility.common.util.SafeCleanerRule;
49 
50 import org.junit.Ignore;
51 import org.junit.Rule;
52 import org.junit.Test;
53 import org.junit.rules.RuleChain;
54 import org.junit.runner.RunWith;
55 
56 import java.util.Collections;
57 import java.util.List;
58 import java.util.concurrent.CountDownLatch;
59 
60 /**
61  * Tests for TextClassifierService query related functions.
62  *
63  * <p>
64  * We use a non-standard TextClassifierService for TextClassifierService-related CTS tests. A
65  * non-standard TextClassifierService that is set via device config. This non-standard
66  * TextClassifierService is not defined in the trust TextClassifierService, it should only receive
67  * queries from clients in the same package.
68  */
69 @RunWith(AndroidJUnit4.class)
70 @Ignore("b/318869191")
71 public class TextClassifierServiceSwapTest {
72     // TODO: Add more tests to verify all the TC APIs call between caller and TCS.
73     private static final String TAG = "TextClassifierServiceSwapTest";
74 
75     private final TextClassifierTestWatcher mTestWatcher =
76             CtsTextClassifierService.getTestWatcher();
77     private final SafeCleanerRule mSafeCleanerRule = mTestWatcher.newSafeCleaner();
78     private final TextClassificationContext mTextClassificationContext =
79             new TextClassificationContext.Builder(
80                     ApplicationProvider.getApplicationContext().getPackageName(),
81                     TextClassifier.WIDGET_TYPE_EDIT_WEBVIEW)
82                     .build();
83     private final RequiredServiceRule mRequiredServiceRule =
84             new RequiredServiceRule(Context.TEXT_CLASSIFICATION_SERVICE);
85 
86     @Rule
87     public final RuleChain mAllRules = RuleChain
88             .outerRule(mRequiredServiceRule)
89             .around(mTestWatcher)
90             .around(mSafeCleanerRule);
91 
92     @Test
testOutsideOfPackageActivity_noRequestReceived()93     public void testOutsideOfPackageActivity_noRequestReceived() throws Exception {
94         // Start an Activity from another package to trigger a TextClassifier call
95         runQueryTextClassifierServiceActivity();
96 
97         // Wait for the TextClassifierService to connect.
98         // Note that the system requires a query to the TextClassifierService before it is
99         // first connected.
100         final CtsTextClassifierService service = mTestWatcher.getService();
101 
102         // Wait a delay for the query is delivered.
103         service.awaitQuery();
104 
105         // Verify the request was not passed to the service.
106         assertThat(service.getRequestSessions()).isEmpty();
107     }
108 
109     @Test
testMultipleActiveSessions()110     public void testMultipleActiveSessions() throws Exception {
111         final TextClassification.Request request =
112                 new TextClassification.Request.Builder("Hello World", 0, 1).build();
113         final TextClassificationManager tcm =
114                 ApplicationProvider.getApplicationContext().getSystemService(
115                         TextClassificationManager.class);
116         final TextClassifier firstSession =
117                 tcm.createTextClassificationSession(mTextClassificationContext);
118         final TextClassifier secondSessionSession =
119                 tcm.createTextClassificationSession(mTextClassificationContext);
120 
121         firstSession.classifyText(request);
122         final CtsTextClassifierService service = mTestWatcher.getService();
123         service.awaitQuery();
124 
125         service.resetRequestLatch(1);
126         secondSessionSession.classifyText(request);
127         service.awaitQuery();
128 
129         final List<TextClassificationSessionId> sessionIds =
130                 service.getRequestSessions().get("onClassifyText");
131         assertThat(sessionIds).hasSize(2);
132         assertThat(sessionIds.get(0).getValue()).isNotEqualTo(sessionIds.get(1).getValue());
133 
134         service.resetRequestLatch(2);
135         firstSession.destroy();
136         secondSessionSession.destroy();
137         service.awaitQuery();
138 
139         final List<TextClassificationSessionId> destroyedSessionIds =
140                 service.getRequestSessions().get("onDestroyTextClassificationSession");
141         assertThat(destroyedSessionIds).hasSize(2);
142         firstSession.isDestroyed();
143         secondSessionSession.isDestroyed();
144     }
145 
serviceOnSuggestConversationActions(CtsTextClassifierService service)146     private void serviceOnSuggestConversationActions(CtsTextClassifierService service)
147             throws Exception {
148         ConversationActions.Request conversationActionRequest =
149                 new ConversationActions.Request.Builder(Collections.emptyList()).build();
150         // TODO: add @TestApi for TextClassificationSessionId and use it
151         SelectionEvent event =
152                 SelectionEvent.createSelectionStartedEvent(
153                         SelectionEvent.INVOCATION_LINK, 1);
154         final CountDownLatch onSuccessLatch = new CountDownLatch(1);
155         service.onSuggestConversationActions(event.getSessionId(),
156                 conversationActionRequest, new CancellationSignal(),
157                 new TextClassifierService.Callback<ConversationActions>() {
158                     @Override
159                     public void onFailure(CharSequence charSequence) {
160                         // do nothing
161                     }
162 
163                     @Override
164                     public void onSuccess(ConversationActions o) {
165                         onSuccessLatch.countDown();
166                     }
167                 });
168         onSuccessLatch.await();
169         final List<TextClassificationSessionId> sessionIds =
170                 service.getRequestSessions().get("onSuggestConversationActions");
171         assertThat(sessionIds).hasSize(1);
172     }
173 
174     @Test
testTextClassifierServiceApiCoverage()175     public void testTextClassifierServiceApiCoverage() throws Exception {
176         // Implemented for API test coverage only
177         // Any method that is better tested not be called here.
178         // We already have tests for the TC APIs
179         // We however need to directly query the TextClassifierService methods in the tests for
180         // code coverage.
181         CtsTextClassifierService service = new CtsTextClassifierService();
182 
183         service.onConnected();
184 
185         serviceOnSuggestConversationActions(service);
186 
187         service.onDisconnected();
188     }
189 
190     @Test
testResourceIconsRewrittenToContentUriIcons_classifyText()191     public void testResourceIconsRewrittenToContentUriIcons_classifyText() throws Exception {
192         final TextClassifier tc = ApplicationProvider.getApplicationContext()
193                 .getSystemService(TextClassificationManager.class)
194                 .getTextClassifier();
195         final TextClassification.Request request =
196                 new TextClassification.Request.Builder("0800 123 4567", 0, 12).build();
197 
198         final TextClassification classification = tc.classifyText(request);
199         final Icon icon = classification.getActions().get(0).getIcon();
200         assertThat(icon.getType()).isEqualTo(Icon.TYPE_URI);
201         assertThat(icon.getUri()).isEqualTo(CtsTextClassifierService.ICON_URI.getUri());
202     }
203 
204     @Test
testResourceIconsRewrittenToContentUriIcons_suggestSelection()205     public void testResourceIconsRewrittenToContentUriIcons_suggestSelection() throws Exception {
206         assumeTrue(BuildCompat.isAtLeastS());
207 
208         final TextClassifier tc = ApplicationProvider.getApplicationContext()
209                 .getSystemService(TextClassificationManager.class)
210                 .getTextClassifier();
211         final TextSelection.Request request =
212                 new TextSelection.Request.Builder("0800 123 4567", 0, 12)
213                         .setIncludeTextClassification(true)
214                         .build();
215 
216         final TextSelection textSelection = tc.suggestSelection(request);
217         final Icon icon = textSelection.getTextClassification().getActions().get(0).getIcon();
218         assertThat(icon.getType()).isEqualTo(Icon.TYPE_URI);
219         assertThat(icon.getUri()).isEqualTo(CtsTextClassifierService.ICON_URI.getUri());
220     }
221 
222     /**
223      * Start an Activity from another package that queries the device's TextClassifierService when
224      * started and immediately terminates itself. When the Activity finishes, it sends broadcast, we
225      * check that whether the finish broadcast is received.
226      */
runQueryTextClassifierServiceActivity()227     private void runQueryTextClassifierServiceActivity() {
228         final String actionQueryActivityFinish =
229                 "ACTION_QUERY_SERVICE_ACTIVITY_FINISH_" + SystemClock.uptimeMillis();
230         final Context context = InstrumentationRegistry.getTargetContext();
231 
232         // register a activity finish receiver
233         final BlockingBroadcastReceiver receiver = new BlockingBroadcastReceiver(context,
234                 actionQueryActivityFinish);
235         receiver.register();
236 
237         // Start an Activity from another package
238         final Intent outsideActivity = new Intent();
239         outsideActivity.setComponent(new ComponentName("android.textclassifier.cts2",
240                 "android.textclassifier.cts2.QueryTextClassifierServiceActivity"));
241         outsideActivity.setFlags(FLAG_ACTIVITY_NEW_TASK);
242         final Intent broadcastIntent = new Intent(actionQueryActivityFinish);
243         final PendingIntent pendingIntent =
244                 PendingIntent.getBroadcast(
245                     context,
246                     0,
247                     broadcastIntent,
248                     PendingIntent.FLAG_IMMUTABLE);
249         outsideActivity.putExtra("finishBroadcast", pendingIntent);
250         context.startActivity(outsideActivity);
251 
252         TextClassifierTestWatcher.waitForIdle();
253 
254         // Verify the finish broadcast is received.
255         final Intent intent = receiver.awaitForBroadcast();
256         assertThat(intent).isNotNull();
257 
258         // unregister receiver
259         receiver.unregisterQuietly();
260     }
261 }
262