1 /*
2  * Copyright (C) 2011 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.content.cts;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 
21 import static org.junit.Assert.assertEquals;
22 import static org.junit.Assert.assertFalse;
23 import static org.junit.Assert.assertNotNull;
24 import static org.junit.Assert.assertNull;
25 import static org.junit.Assert.assertTrue;
26 import static org.junit.Assume.assumeTrue;
27 
28 import android.Manifest;
29 import android.app.Activity;
30 import android.app.UiAutomation;
31 import android.content.ClipData;
32 import android.content.ClipData.Item;
33 import android.content.ClipDescription;
34 import android.content.ClipboardManager;
35 import android.content.ClipboardManager.OnPrimaryClipChangedListener;
36 import android.content.ContentResolver;
37 import android.content.Context;
38 import android.content.Intent;
39 import android.content.pm.PackageManager;
40 import android.net.Uri;
41 import android.platform.test.annotations.AppModeNonSdkSandbox;
42 import android.platform.test.annotations.IgnoreUnderRavenwood;
43 import android.platform.test.ravenwood.RavenwoodRule;
44 
45 import androidx.test.InstrumentationRegistry;
46 import androidx.test.runner.AndroidJUnit4;
47 import androidx.test.uiautomator.By;
48 import androidx.test.uiautomator.UiDevice;
49 import androidx.test.uiautomator.Until;
50 
51 import com.android.compatibility.common.util.SystemUtil;
52 
53 import org.junit.After;
54 import org.junit.Before;
55 import org.junit.Rule;
56 import org.junit.Test;
57 import org.junit.runner.RunWith;
58 
59 import java.util.concurrent.CountDownLatch;
60 import java.util.concurrent.TimeUnit;
61 
62 @RunWith(AndroidJUnit4.class)
63 //@AppModeFull // TODO(Instant) Should clip board data be visible?
64 @AppModeNonSdkSandbox(reason = "SDK sandboxes cannot access ClipboardManager.")
65 public class ClipboardManagerTest {
66     @Rule
67     public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
68             .setProvideMainThread(true)
69             .setPackageName("android.content.cts")
70             .setServicesRequired(ClipboardManager.class)
71             .build();
72 
73     private Context mContext;
74     private ClipboardManager mClipboardManager;
75     private UiDevice mUiDevice;
76 
77     @Before
setUp()78     public void setUp() throws Exception {
79         assumeTrue("Skipping Test: Wear-Os does not support ClipboardService", hasAutoFillFeature());
80 
81         mContext = InstrumentationRegistry.getTargetContext();
82         mClipboardManager = mContext.getSystemService(ClipboardManager.class);
83 
84         if (!RavenwoodRule.isOnRavenwood()) {
85             mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
86             mUiDevice.wakeUp();
87 
88             // Clear any dialogs and launch an activity as focus is needed to access clipboard.
89             mUiDevice.pressHome();
90             mUiDevice.pressBack();
91             launchActivity(MockActivity.class);
92         }
93     }
94 
95     @After
cleanUp()96     public void cleanUp() {
97         if (mClipboardManager != null) {
98             mClipboardManager.clearPrimaryClip();
99         }
100         dropShellPermissionIdentity();
101     }
102 
103     @Test
testSetGetText()104     public void testSetGetText() {
105         ClipboardManager clipboardManager = mClipboardManager;
106         clipboardManager.setText("Test Text 1");
107         assertEquals("Test Text 1", clipboardManager.getText());
108 
109         clipboardManager.setText("Test Text 2");
110         assertEquals("Test Text 2", clipboardManager.getText());
111     }
112 
113     @Test
testHasPrimaryClip()114     public void testHasPrimaryClip() {
115         ClipboardManager clipboardManager = mClipboardManager;
116         if (clipboardManager.hasPrimaryClip()) {
117             assertNotNull(clipboardManager.getPrimaryClip());
118             assertNotNull(clipboardManager.getPrimaryClipDescription());
119         } else {
120             assertNull(clipboardManager.getPrimaryClip());
121             assertNull(clipboardManager.getPrimaryClipDescription());
122         }
123 
124         clipboardManager.setPrimaryClip(ClipData.newPlainText("Label", "Text"));
125         assertTrue(clipboardManager.hasPrimaryClip());
126     }
127 
128     @Test
testSetPrimaryClip_plainText()129     public void testSetPrimaryClip_plainText() {
130         ClipData textData = ClipData.newPlainText("TextLabel", "Text");
131         assertSetPrimaryClip(textData, "TextLabel",
132                 new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN},
133                 new ExpectedClipItem("Text", null, null));
134     }
135 
136     @Test
testSetPrimaryClip_intent()137     public void testSetPrimaryClip_intent() {
138         Intent intent = new Intent(mContext, ClipboardManagerTest.class);
139         ClipData intentData = ClipData.newIntent("IntentLabel", intent);
140         assertSetPrimaryClip(intentData, "IntentLabel",
141                 new String[] {ClipDescription.MIMETYPE_TEXT_INTENT},
142                 new ExpectedClipItem(null, intent, null));
143     }
144 
145     @Test
testSetPrimaryClip_rawUri()146     public void testSetPrimaryClip_rawUri() {
147         Uri uri = Uri.parse("http://www.google.com");
148         ClipData uriData = ClipData.newRawUri("UriLabel", uri);
149         assertSetPrimaryClip(uriData, "UriLabel",
150                 new String[] {ClipDescription.MIMETYPE_TEXT_URILIST},
151                 new ExpectedClipItem(null, null, uri));
152     }
153 
154     @Test
155     @IgnoreUnderRavenwood(blockedBy = ContentResolver.class)
testSetPrimaryClip_contentUri()156     public void testSetPrimaryClip_contentUri() {
157         Uri contentUri = Uri.parse("content://cts/test/for/clipboardmanager");
158         ClipData contentUriData = ClipData.newUri(mContext.getContentResolver(),
159                 "ContentUriLabel", contentUri);
160         assertSetPrimaryClip(contentUriData, "ContentUriLabel",
161                 new String[] {ClipDescription.MIMETYPE_TEXT_URILIST},
162                 new ExpectedClipItem(null, null, contentUri));
163     }
164 
165     @Test
testSetPrimaryClip_complexItem()166     public void testSetPrimaryClip_complexItem() {
167         Intent intent = new Intent(mContext, ClipboardManagerTest.class);
168         Uri uri = Uri.parse("http://www.google.com");
169         ClipData multiData = new ClipData(new ClipDescription("ComplexItemLabel",
170                 new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN,
171                         ClipDescription.MIMETYPE_TEXT_INTENT,
172                         ClipDescription.MIMETYPE_TEXT_URILIST}),
173                 new Item("Text", intent, uri));
174         assertSetPrimaryClip(multiData, "ComplexItemLabel",
175                 new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN,
176                         ClipDescription.MIMETYPE_TEXT_INTENT,
177                         ClipDescription.MIMETYPE_TEXT_URILIST},
178                 new ExpectedClipItem("Text", intent, uri));
179     }
180 
181     @Test
testSetPrimaryClip_multipleItems()182     public void testSetPrimaryClip_multipleItems() {
183         Intent intent = new Intent(mContext, ClipboardManagerTest.class);
184         Uri uri = Uri.parse("http://www.google.com");
185         ClipData textData = ClipData.newPlainText("TextLabel", "Text");
186         textData.addItem(new Item("More Text"));
187         textData.addItem(new Item(intent));
188         textData.addItem(new Item(uri));
189         assertSetPrimaryClip(textData, "TextLabel",
190                 new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN},
191                 new ExpectedClipItem("Text", null, null),
192                 new ExpectedClipItem("More Text", null, null),
193                 new ExpectedClipItem(null, intent, null),
194                 new ExpectedClipItem(null, null, uri));
195     }
196 
197     @Test
198     @IgnoreUnderRavenwood(blockedBy = ContentResolver.class)
testSetPrimaryClip_multipleMimeTypes()199     public void testSetPrimaryClip_multipleMimeTypes() {
200         ContentResolver contentResolver = mContext.getContentResolver();
201 
202         Intent intent = new Intent(mContext, ClipboardManagerTest.class);
203         Uri uri = Uri.parse("http://www.google.com");
204         Uri contentUri1 = Uri.parse("content://ctstest/testtable1");
205         Uri contentUri2 = Uri.parse("content://ctstest/testtable2");
206         Uri contentUri3 = Uri.parse("content://ctstest/testtable1/0");
207         Uri contentUri4 = Uri.parse("content://ctstest/testtable1/1");
208         Uri contentUri5 = Uri.parse("content://ctstest/testtable2/0");
209         Uri contentUri6 = Uri.parse("content://ctstest/testtable2/1");
210         Uri contentUri7 = Uri.parse("content://ctstest/testtable2/2");
211         Uri contentUri8 = Uri.parse("content://ctstest/testtable2/3");
212 
213         ClipData clipData = ClipData.newPlainText("TextLabel", "Text");
214         clipData.addItem(contentResolver, new Item("More Text"));
215         clipData.addItem(contentResolver, new Item(intent));
216         clipData.addItem(contentResolver, new Item(uri));
217         clipData.addItem(contentResolver, new Item(contentUri1));
218         clipData.addItem(contentResolver, new Item(contentUri2));
219         clipData.addItem(contentResolver, new Item(contentUri3));
220         clipData.addItem(contentResolver, new Item(contentUri4));
221         clipData.addItem(contentResolver, new Item(contentUri5));
222         clipData.addItem(contentResolver, new Item(contentUri6));
223         clipData.addItem(contentResolver, new Item(contentUri7));
224         clipData.addItem(contentResolver, new Item(contentUri8));
225 
226         assertClipData(clipData, "TextLabel",
227                 new String[] {
228                         ClipDescription.MIMETYPE_TEXT_PLAIN,
229                         ClipDescription.MIMETYPE_TEXT_INTENT,
230                         ClipDescription.MIMETYPE_TEXT_URILIST,
231                         "vnd.android.cursor.dir/com.android.content.testtable1",
232                         "vnd.android.cursor.dir/com.android.content.testtable2",
233                         "vnd.android.cursor.item/com.android.content.testtable1",
234                         "vnd.android.cursor.item/com.android.content.testtable2",
235                         "image/jpeg",
236                         "audio/mpeg",
237                         "video/mpeg"
238                 },
239                 new ExpectedClipItem("Text", null, null),
240                 new ExpectedClipItem("More Text", null, null),
241                 new ExpectedClipItem(null, intent, null),
242                 new ExpectedClipItem(null, null, uri),
243                 new ExpectedClipItem(null, null, contentUri1),
244                 new ExpectedClipItem(null, null, contentUri2),
245                 new ExpectedClipItem(null, null, contentUri3),
246                 new ExpectedClipItem(null, null, contentUri4),
247                 new ExpectedClipItem(null, null, contentUri5),
248                 new ExpectedClipItem(null, null, contentUri6),
249                 new ExpectedClipItem(null, null, contentUri7),
250                 new ExpectedClipItem(null, null, contentUri8));
251     }
252 
253     @Test
testPrimaryClipChangedListener()254     public void testPrimaryClipChangedListener() throws Exception {
255         final CountDownLatch latch = new CountDownLatch(1);
256         mClipboardManager.addPrimaryClipChangedListener(new OnPrimaryClipChangedListener() {
257             @Override
258             public void onPrimaryClipChanged() {
259                 latch.countDown();
260             }
261         });
262 
263         final ClipData clipData = ClipData.newPlainText("TextLabel", "Text");
264         mClipboardManager.setPrimaryClip(clipData);
265 
266         latch.await(5, TimeUnit.SECONDS);
267     }
268 
269     @Test
testClearPrimaryClip()270     public void testClearPrimaryClip() {
271         final ClipData clipData = ClipData.newPlainText("TextLabel", "Text");
272         mClipboardManager.setPrimaryClip(clipData);
273         assertTrue(mClipboardManager.hasPrimaryClip());
274         assertTrue(mClipboardManager.hasText());
275         assertNotNull(mClipboardManager.getPrimaryClip());
276         assertNotNull(mClipboardManager.getPrimaryClipDescription());
277 
278         mClipboardManager.clearPrimaryClip();
279         assertFalse(mClipboardManager.hasPrimaryClip());
280         assertFalse(mClipboardManager.hasText());
281         assertNull(mClipboardManager.getPrimaryClip());
282         assertNull(mClipboardManager.getPrimaryClipDescription());
283     }
284 
285     @Test
286     @IgnoreUnderRavenwood(blockedBy = UiAutomation.class)
testPrimaryClipNotAvailableWithoutFocus()287     public void testPrimaryClipNotAvailableWithoutFocus() throws Exception {
288         ClipData textData = ClipData.newPlainText("TextLabel", "Text1");
289         assertSetPrimaryClip(textData, "TextLabel",
290                 new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN},
291                 new ExpectedClipItem("Text1", null, null));
292 
293         // Press the home button to unfocus the app.
294         mUiDevice.pressHome();
295         mUiDevice.wait(Until.gone(By.pkg(MockActivity.class.getPackageName())), 5000);
296 
297         // We should see an empty clipboard now.
298         assertFalse(mClipboardManager.hasPrimaryClip());
299         assertFalse(mClipboardManager.hasText());
300         assertNull(mClipboardManager.getPrimaryClip());
301         assertNull(mClipboardManager.getPrimaryClipDescription());
302 
303         // We should be able to set the clipboard but not see the contents.
304         mClipboardManager.setPrimaryClip(ClipData.newPlainText("TextLabel", "Text2"));
305         assertFalse(mClipboardManager.hasPrimaryClip());
306         assertFalse(mClipboardManager.hasText());
307         assertNull(mClipboardManager.getPrimaryClip());
308         assertNull(mClipboardManager.getPrimaryClipDescription());
309 
310         // Launch an activity to get back in focus.
311         launchActivity(MockActivity.class);
312 
313         // Verify clipboard access is restored.
314         assertNotNull(mClipboardManager.getPrimaryClip());
315         assertNotNull(mClipboardManager.getPrimaryClipDescription());
316 
317         // Verify we were unable to change the clipboard while out of focus.
318         assertClipData(mClipboardManager.getPrimaryClip(),
319                 "TextLabel",
320                 new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN},
321                 new ExpectedClipItem("Text2", null, null));
322     }
323 
324     @Test
325     @IgnoreUnderRavenwood(blockedBy = UiAutomation.class)
testReadInBackgroundRequiresPermission()326     public void testReadInBackgroundRequiresPermission() throws Exception {
327         ClipData clip = ClipData.newPlainText("TextLabel", "Text1");
328         mClipboardManager.setPrimaryClip(clip);
329 
330         // Press the home button to unfocus the app.
331         mUiDevice.pressHome();
332         mUiDevice.wait(Until.gone(By.pkg(MockActivity.class.getPackageName())), 5000);
333 
334         // Without the READ_CLIPBOARD_IN_BACKGROUND permission, we should see an empty clipboard.
335         assertThat(mClipboardManager.hasPrimaryClip()).isFalse();
336         assertThat(mClipboardManager.hasText()).isFalse();
337         assertThat(mClipboardManager.getPrimaryClip()).isNull();
338         assertThat(mClipboardManager.getPrimaryClipDescription()).isNull();
339 
340         // Having the READ_CLIPBOARD_IN_BACKGROUND permission should allow us to read the clipboard
341         // even when we are not in the foreground. We use the shell identity to simulate holding
342         // this permission; in practice, only privileged system apps can hold this permission (e.g.
343         // an app that has the SYSTEM_TEXT_INTELLIGENCE role).
344         ClipData actual = SystemUtil.callWithShellPermissionIdentity(
345                 () -> mClipboardManager.getPrimaryClip(),
346                 android.Manifest.permission.READ_CLIPBOARD_IN_BACKGROUND);
347         assertThat(actual).isNotNull();
348         assertThat(actual.getItemAt(0).getText()).isEqualTo("Text1");
349     }
350 
351     @Test
testClipSourceRecordedWhenClipSet()352     public void testClipSourceRecordedWhenClipSet() {
353         ClipData clipData = ClipData.newPlainText("TextLabel", "Text1");
354         mClipboardManager.setPrimaryClip(clipData);
355 
356         adoptShellPermissionIdentity(Manifest.permission.SET_CLIP_SOURCE);
357         assertThat(
358                 mClipboardManager.getPrimaryClipSource()).isEqualTo("android.content.cts");
359     }
360 
361     @Test
testSetPrimaryClipAsPackage()362     public void testSetPrimaryClipAsPackage() {
363         adoptShellPermissionIdentity(Manifest.permission.SET_CLIP_SOURCE);
364 
365         ClipData clipData = ClipData.newPlainText("TextLabel", "Text1");
366         mClipboardManager.setPrimaryClipAsPackage(clipData, "test.package");
367 
368         assertThat(
369                 mClipboardManager.getPrimaryClipSource()).isEqualTo("test.package");
370     }
371 
launchActivity(Class<? extends Activity> clazz)372     private void launchActivity(Class<? extends Activity> clazz) {
373         Intent intent = new Intent(Intent.ACTION_MAIN);
374         intent.setClassName(mContext.getPackageName(), clazz.getName());
375         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
376         mContext.startActivity(intent);
377         mUiDevice.wait(Until.hasObject(By.pkg(clazz.getPackageName())), 15000);
378     }
379 
380     private class ExpectedClipItem {
381         CharSequence mText;
382         Intent mIntent;
383         Uri mUri;
384 
ExpectedClipItem(CharSequence text, Intent intent, Uri uri)385         ExpectedClipItem(CharSequence text, Intent intent, Uri uri) {
386             mText = text;
387             mIntent = intent;
388             mUri = uri;
389         }
390     }
391 
assertSetPrimaryClip(ClipData clipData, String expectedLabel, String[] expectedMimeTypes, ExpectedClipItem... expectedClipItems)392     private void assertSetPrimaryClip(ClipData clipData,
393             String expectedLabel,
394             String[] expectedMimeTypes,
395             ExpectedClipItem... expectedClipItems) {
396         ClipboardManager clipboardManager = mClipboardManager;
397 
398         clipboardManager.setPrimaryClip(clipData);
399         assertTrue(clipboardManager.hasPrimaryClip());
400 
401         if (expectedClipItems != null
402                 && expectedClipItems.length > 0
403                 && expectedClipItems[0].mText != null) {
404             assertTrue(clipboardManager.hasText());
405         } else {
406             assertFalse(clipboardManager.hasText());
407         }
408 
409         assertNotNull(clipboardManager.getPrimaryClip());
410         assertNotNull(clipboardManager.getPrimaryClipDescription());
411 
412         assertClipData(clipboardManager.getPrimaryClip(),
413                 expectedLabel, expectedMimeTypes, expectedClipItems);
414 
415         assertClipDescription(clipboardManager.getPrimaryClipDescription(),
416                 expectedLabel, expectedMimeTypes);
417     }
418 
assertClipData(ClipData actualData, String expectedLabel, String[] expectedMimeTypes, ExpectedClipItem... expectedClipItems)419     private static void assertClipData(ClipData actualData, String expectedLabel,
420             String[] expectedMimeTypes, ExpectedClipItem... expectedClipItems) {
421         if (expectedClipItems != null) {
422             assertEquals(expectedClipItems.length, actualData.getItemCount());
423             for (int i = 0; i < expectedClipItems.length; i++) {
424                 assertClipItem(expectedClipItems[i], actualData.getItemAt(i));
425             }
426         } else {
427             throw new IllegalArgumentException("Should have at least one expectedClipItem...");
428         }
429 
430         assertClipDescription(actualData.getDescription(), expectedLabel, expectedMimeTypes);
431     }
432 
assertClipDescription(ClipDescription description, String expectedLabel, String... mimeTypes)433     private static void assertClipDescription(ClipDescription description, String expectedLabel,
434             String... mimeTypes) {
435         assertEquals(expectedLabel, description.getLabel());
436         assertEquals(mimeTypes.length, description.getMimeTypeCount());
437         int mimeTypeCount = description.getMimeTypeCount();
438         for (int i = 0; i < mimeTypeCount; i++) {
439             assertEquals(mimeTypes[i], description.getMimeType(i));
440         }
441     }
442 
assertClipItem(ExpectedClipItem expectedItem, Item item)443     private static void assertClipItem(ExpectedClipItem expectedItem, Item item) {
444         assertEquals(expectedItem.mText, item.getText());
445         if (expectedItem.mIntent != null) {
446             assertNotNull(item.getIntent());
447         } else {
448             assertNull(item.getIntent());
449         }
450         if (expectedItem.mUri != null) {
451             assertEquals(expectedItem.mUri.toString(), item.getUri().toString());
452         } else {
453             assertNull(item.getUri());
454         }
455     }
456 
hasAutoFillFeature()457     private boolean hasAutoFillFeature() {
458         if (RavenwoodRule.isOnRavenwood()) {
459             // These tests awkwardly depend on FEATURE_AUTOFILL to detect clipboard support;
460             // even though Ravenwood doesn't support autofill feature, we know we support
461             // clipboard, so we return true so tests are executed
462             return true;
463         } else {
464             return InstrumentationRegistry.getTargetContext().getPackageManager()
465                     .hasSystemFeature(PackageManager.FEATURE_AUTOFILL);
466         }
467     }
468 
adoptShellPermissionIdentity(String permission)469     private static void adoptShellPermissionIdentity(String permission) {
470         if (RavenwoodRule.isOnRavenwood()) {
471             // TODO: define what "shell permissions" mean on Ravenwood, and offer
472             // a general adoptShellPermissionIdentity implementation; ignored for now
473         } else {
474             InstrumentationRegistry.getInstrumentation().getUiAutomation()
475                     .adoptShellPermissionIdentity(permission);
476         }
477     }
478 
dropShellPermissionIdentity()479     private static void dropShellPermissionIdentity() {
480         if (RavenwoodRule.isOnRavenwood()) {
481             // TODO: define what "shell permissions" mean on Ravenwood, and offer
482             // a general adoptShellPermissionIdentity implementation; ignored for now
483         } else {
484             InstrumentationRegistry.getInstrumentation().getUiAutomation()
485                     .dropShellPermissionIdentity();
486         }
487     }
488 }
489