• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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.providerui.cts;
18 
19 import static android.Manifest.permission.ACCESS_BACKGROUND_LOCATION;
20 import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
21 import static android.Manifest.permission.ACCESS_FINE_LOCATION;
22 import static android.Manifest.permission.ACCESS_MEDIA_LOCATION;
23 import static android.Manifest.permission.CAMERA;
24 import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
25 import static android.Manifest.permission.RECORD_AUDIO;
26 import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
27 
28 import static org.junit.Assert.assertArrayEquals;
29 import static org.junit.Assert.assertEquals;
30 import static org.junit.Assert.assertFalse;
31 import static org.junit.Assert.assertNotNull;
32 import static org.junit.Assert.assertTrue;
33 import static org.junit.Assert.fail;
34 
35 import android.app.Activity;
36 import android.app.Instrumentation;
37 import android.app.UiAutomation;
38 import android.content.ContentResolver;
39 import android.content.Context;
40 import android.content.Intent;
41 import android.content.UriPermission;
42 import android.content.pm.PackageInfo;
43 import android.content.pm.PackageManager;
44 import android.content.pm.PackageManager.NameNotFoundException;
45 import android.content.pm.PermissionInfo;
46 import android.content.pm.ResolveInfo;
47 import android.content.res.AssetFileDescriptor;
48 import android.media.ExifInterface;
49 import android.net.Uri;
50 import android.os.Environment;
51 import android.os.FileUtils;
52 import android.os.ParcelFileDescriptor;
53 import android.os.SystemClock;
54 import android.os.storage.StorageManager;
55 import android.os.storage.StorageVolume;
56 import android.provider.MediaStore;
57 import android.providerui.cts.GetResultActivity.Result;
58 import android.support.test.uiautomator.By;
59 import android.support.test.uiautomator.BySelector;
60 import android.support.test.uiautomator.UiDevice;
61 import android.support.test.uiautomator.UiObject2;
62 import android.support.test.uiautomator.UiSelector;
63 import android.support.test.uiautomator.Until;
64 import android.system.Os;
65 import android.text.format.DateUtils;
66 import android.util.Log;
67 import android.view.KeyEvent;
68 
69 import androidx.core.content.FileProvider;
70 import androidx.test.InstrumentationRegistry;
71 
72 import org.junit.After;
73 import org.junit.Before;
74 import org.junit.Test;
75 import org.junit.runner.RunWith;
76 import org.junit.runners.Parameterized;
77 import org.junit.runners.Parameterized.Parameter;
78 import org.junit.runners.Parameterized.Parameters;
79 
80 import java.io.BufferedReader;
81 import java.io.File;
82 import java.io.FileInputStream;
83 import java.io.FileNotFoundException;
84 import java.io.FileOutputStream;
85 import java.io.IOException;
86 import java.io.InputStream;
87 import java.io.InputStreamReader;
88 import java.io.OutputStream;
89 import java.nio.charset.StandardCharsets;
90 import java.text.SimpleDateFormat;
91 import java.util.Arrays;
92 import java.util.Date;
93 import java.util.HashSet;
94 import java.util.Set;
95 import java.util.concurrent.TimeUnit;
96 
97 @RunWith(Parameterized.class)
98 public class MediaStoreUiTest {
99     private static final String TAG = "MediaStoreUiTest";
100 
101     private static final int REQUEST_CODE = 42;
102 
103     private Instrumentation mInstrumentation;
104     private Context mContext;
105     private UiDevice mDevice;
106     private GetResultActivity mActivity;
107 
108     private File mFile;
109     private Uri mMediaStoreUri;
110     private String mTargetPackageName;
111 
112     @Parameter(0)
113     public String mVolumeName;
114 
115     @Parameters
data()116     public static Iterable<? extends Object> data() {
117         return MediaStore.getExternalVolumeNames(InstrumentationRegistry.getTargetContext());
118     }
119 
120     @Before
setUp()121     public void setUp() throws Exception {
122         mInstrumentation = InstrumentationRegistry.getInstrumentation();
123         mContext = InstrumentationRegistry.getTargetContext();
124         mDevice = UiDevice.getInstance(mInstrumentation);
125 
126         final Intent intent = new Intent(mContext, GetResultActivity.class);
127         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
128         mActivity = (GetResultActivity) mInstrumentation.startActivitySync(intent);
129         mInstrumentation.waitForIdleSync();
130         mActivity.clearResult();
131     }
132 
133     @After
tearDown()134     public void tearDown() throws Exception {
135         if (mFile != null) {
136             mFile.delete();
137         }
138 
139         final ContentResolver resolver = mActivity.getContentResolver();
140         for (UriPermission permission : resolver.getPersistedUriPermissions()) {
141             mActivity.revokeUriPermission(
142                     permission.getUri(),
143                     Intent.FLAG_GRANT_READ_URI_PERMISSION
144                         | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
145         }
146 
147         mActivity.finish();
148     }
149 
150     @Test
testGetDocumentUri()151     public void testGetDocumentUri() throws Exception {
152         if (!supportsHardware()) return;
153 
154         prepareFile();
155 
156         final Uri treeUri = acquireAccess(mFile, Environment.DIRECTORY_DOCUMENTS);
157         assertNotNull(treeUri);
158 
159         final Uri docUri = MediaStore.getDocumentUri(mActivity, mMediaStoreUri);
160         assertNotNull(docUri);
161 
162         final ContentResolver resolver = mActivity.getContentResolver();
163 
164         // Test reading
165         final byte[] expected = "TEST".getBytes();
166         final byte[] actual = new byte[4];
167         try (ParcelFileDescriptor fd = resolver.openFileDescriptor(docUri, "r")) {
168             Os.read(fd.getFileDescriptor(), actual, 0, actual.length);
169             assertArrayEquals(expected, actual);
170         }
171 
172         // Test writing
173         try (ParcelFileDescriptor fd = resolver.openFileDescriptor(docUri, "wt")) {
174             Os.write(fd.getFileDescriptor(), expected, 0, expected.length);
175         }
176     }
177 
178     @Test
testGetDocumentUri_ThrowsWithoutPermission()179     public void testGetDocumentUri_ThrowsWithoutPermission() throws Exception {
180         if (!supportsHardware()) return;
181 
182         prepareFile();
183 
184         try {
185             MediaStore.getDocumentUri(mActivity, mMediaStoreUri);
186             fail("Expecting SecurityException.");
187         } catch (SecurityException e) {
188             // Expected
189         }
190     }
191 
192     @Test
testGetDocumentUri_Symmetry()193     public void testGetDocumentUri_Symmetry() throws Exception {
194         if (!supportsHardware()) return;
195 
196         prepareFile();
197 
198         final Uri treeUri = acquireAccess(mFile, Environment.DIRECTORY_DOCUMENTS);
199         Log.v(TAG, "Tree " + treeUri);
200         assertNotNull(treeUri);
201 
202         final Uri docUri = MediaStore.getDocumentUri(mActivity, mMediaStoreUri);
203         Log.v(TAG, "Document " + docUri);
204         assertNotNull(docUri);
205 
206         final Uri mediaUri = MediaStore.getMediaUri(mActivity, docUri);
207         Log.v(TAG, "Media " + mediaUri);
208         assertNotNull(mediaUri);
209 
210         assertEquals(mMediaStoreUri, mediaUri);
211     }
212 
maybeClick(UiSelector sel)213     private void maybeClick(UiSelector sel) {
214         try { mDevice.findObject(sel).click(); } catch (Throwable ignored) { }
215     }
216 
maybeClick(BySelector sel)217     private void maybeClick(BySelector sel) {
218         try { mDevice.findObject(sel).click(); } catch (Throwable ignored) { }
219     }
220 
maybeGrantRuntimePermission(String pkg, Set<String> requested, String permission)221     private void maybeGrantRuntimePermission(String pkg, Set<String> requested, String permission)
222             throws NameNotFoundException {
223         // We only need to grant dangerous permissions
224         if ((mContext.getPackageManager().getPermissionInfo(permission, 0).getProtection()
225                 & PermissionInfo.PROTECTION_DANGEROUS) == 0) {
226             return;
227         }
228 
229         if (requested.contains(permission)) {
230             InstrumentationRegistry.getInstrumentation().getUiAutomation()
231                     .grantRuntimePermission(pkg, permission);
232         }
233     }
234 
235     /**
236      * Verify that whoever handles {@link MediaStore#ACTION_IMAGE_CAPTURE} can
237      * correctly write the contents into a passed {@code content://} Uri.
238      */
239     @Test
testImageCaptureWithInadequeteLocationPermissions()240     public void testImageCaptureWithInadequeteLocationPermissions() throws Exception {
241         Set<String> perms = new HashSet<>();
242         perms.add(ACCESS_COARSE_LOCATION);
243         perms.add(ACCESS_BACKGROUND_LOCATION);
244         perms.add(ACCESS_MEDIA_LOCATION);
245         testImageCaptureWithoutLocation(perms);
246     }
247      /**
248      * Helper function to verify that whoever handles {@link MediaStore#ACTION_IMAGE_CAPTURE} can
249      * correctly write the contents into a passed {@code content://} Uri, without location
250      * information, necessarily, when ACCESS_FINE_LOCATION permissions aren't given.
251      */
testImageCaptureWithoutLocation(Set<String> locationPermissions)252     private void testImageCaptureWithoutLocation(Set<String> locationPermissions)
253             throws Exception {
254         assertFalse("testImageCaptureWithoutLocation should not be passed ACCESS_FINE_LOCATION",
255                 locationPermissions.contains(ACCESS_FINE_LOCATION));
256         if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)) {
257             Log.d(TAG, "Skipping due to lack of camera");
258             return;
259         }
260 
261         String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
262 
263         final File targetDir = new File(mContext.getFilesDir(), "debug");
264         final File target = new File(targetDir, timeStamp  + "capture.jpg");
265 
266         targetDir.mkdirs();
267         assertFalse(target.exists());
268 
269         final Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
270         intent.putExtra(MediaStore.EXTRA_OUTPUT,
271                 FileProvider.getUriForFile(mContext, "android.providerui.cts.fileprovider", target));
272 
273         // Figure out who is going to answer the phone
274         final ResolveInfo ri = mContext.getPackageManager().resolveActivity(intent, 0);
275         final String answeringPkg = ri.activityInfo.packageName;
276         Log.d(TAG, "We're probably launching " + ri);
277 
278         final PackageInfo pi = mContext.getPackageManager().getPackageInfo(answeringPkg,
279                 PackageManager.GET_PERMISSIONS);
280         final Set<String> answeringReq = new HashSet<>();
281         answeringReq.addAll(Arrays.asList(pi.requestedPermissions));
282         // Grant the 'answering' app all the permissions they might want.
283         maybeGrantRuntimePermission(answeringPkg, answeringReq, CAMERA);
284         maybeGrantRuntimePermission(answeringPkg, answeringReq, ACCESS_FINE_LOCATION);
285         maybeGrantRuntimePermission(answeringPkg, answeringReq, ACCESS_COARSE_LOCATION);
286         maybeGrantRuntimePermission(answeringPkg, answeringReq, ACCESS_BACKGROUND_LOCATION);
287         maybeGrantRuntimePermission(answeringPkg, answeringReq, RECORD_AUDIO);
288         maybeGrantRuntimePermission(answeringPkg, answeringReq, READ_EXTERNAL_STORAGE);
289         maybeGrantRuntimePermission(answeringPkg, answeringReq, WRITE_EXTERNAL_STORAGE);
290         SystemClock.sleep(DateUtils.SECOND_IN_MILLIS);
291 
292         grantSelfRequisitePermissions(locationPermissions);
293 
294         Result result = getImageCaptureIntentResult(intent, answeringPkg);
295 
296         assertTrue("exists", target.exists());
297         assertTrue("has data", target.length() > 65536);
298 
299         // At the very least we expect photos generated by the device to have
300         // sane baseline EXIF data
301         final ExifInterface exif = new ExifInterface(new FileInputStream(target));
302         assertAttribute(exif, ExifInterface.TAG_MAKE);
303         assertAttribute(exif, ExifInterface.TAG_MODEL);
304         assertAttribute(exif, ExifInterface.TAG_DATETIME);
305         float[] latLong = new float[2];
306         Boolean hasLocation = exif.getLatLong(latLong);
307         assertTrue("Should not contain location information latitude: " + latLong[0] +
308                 " longitude: " + latLong[1], !hasLocation);
309     }
310 
grantSelfRequisitePermissions(Set<String> locationPermissions)311     private void grantSelfRequisitePermissions(Set<String> locationPermissions)
312             throws Exception {
313         String selfPkg = mContext.getPackageName();
314         for (String perm : locationPermissions) {
315             InstrumentationRegistry.getInstrumentation().getUiAutomation()
316                     .grantRuntimePermission(selfPkg, perm);
317             assertTrue("Permission " + perm + "could not be granted",
318                     mContext.checkSelfPermission(perm) == PackageManager.PERMISSION_GRANTED);
319         }
320     }
321 
getImageCaptureIntentResult(Intent intent, String answeringPkg)322     private Result getImageCaptureIntentResult(Intent intent, String answeringPkg)
323             throws Exception {
324 
325         mActivity.startActivityForResult(intent, REQUEST_CODE);
326         mDevice.waitForIdle();
327 
328         // To ensure camera app is launched
329         SystemClock.sleep(5 * DateUtils.SECOND_IN_MILLIS);
330 
331         // Try a couple different strategies for taking a photo / capturing a video: first capture
332         // and confirm using hardware keys.
333         mDevice.pressKeyCode(KeyEvent.KEYCODE_CAMERA);
334         mDevice.waitForIdle();
335         SystemClock.sleep(5 * DateUtils.SECOND_IN_MILLIS);
336         // We're done.
337         mDevice.pressKeyCode(KeyEvent.KEYCODE_DPAD_CENTER);
338         mDevice.waitForIdle();
339 
340         // Maybe that gave us a result?
341         Result result = mActivity.getResult(15, TimeUnit.SECONDS);
342         Log.d(TAG, "First pass result was " + result);
343 
344         // Hrm, that didn't work; let's try an alternative approach of digging
345         // around for a shutter button
346         if (result == null) {
347             maybeClick(new UiSelector().resourceId(answeringPkg + ":id/shutter_button"));
348             mDevice.waitForIdle();
349             SystemClock.sleep(5 * DateUtils.SECOND_IN_MILLIS);
350             maybeClick(new UiSelector().resourceId(answeringPkg + ":id/shutter_button"));
351             mDevice.waitForIdle();
352             maybeClick(new UiSelector().resourceId(answeringPkg + ":id/done_button"));
353             mDevice.waitForIdle();
354 
355             result = mActivity.getResult(15, TimeUnit.SECONDS);
356             Log.d(TAG, "Second pass result was " + result);
357         }
358 
359         // Grr, let's try hunting around even more
360         if (result == null) {
361             maybeClick(By.pkg(answeringPkg).descContains("Capture"));
362             mDevice.waitForIdle();
363             SystemClock.sleep(5 * DateUtils.SECOND_IN_MILLIS);
364             maybeClick(By.pkg(answeringPkg).descContains("Done"));
365             mDevice.waitForIdle();
366 
367             result = mActivity.getResult(15, TimeUnit.SECONDS);
368             Log.d(TAG, "Third pass result was " + result);
369         }
370 
371         assertNotNull("Expected to get a IMAGE_CAPTURE result; your camera app should "
372                 + "respond to the CAMERA and DPAD_CENTER keycodes", result);
373         return result;
374     }
375 
assertAttribute(ExifInterface exif, String tag)376     private static void assertAttribute(ExifInterface exif, String tag) {
377         final String res = exif.getAttribute(tag);
378         if (res == null || res.length() == 0) {
379             Log.d(TAG, "Expected valid EXIF tag for tag " + tag);
380         }
381     }
382 
supportsHardware()383     private boolean supportsHardware() {
384         final PackageManager pm = mContext.getPackageManager();
385         return !pm.hasSystemFeature("android.hardware.type.television")
386                 && !pm.hasSystemFeature("android.hardware.type.watch");
387     }
388 
prepareFile()389     private void prepareFile() throws Exception {
390         final File dir = new File(MediaStore.getVolumePath(mVolumeName),
391                 Environment.DIRECTORY_DOCUMENTS);
392         final File file = new File(dir, "cts" + System.nanoTime() + ".txt");
393 
394         mFile = stageFile(R.raw.text, file);
395         mMediaStoreUri = MediaStore.scanFile(mContext, mFile);
396 
397         Log.v(TAG, "Staged " + mFile + " as " + mMediaStoreUri);
398     }
399 
acquireAccess(File file, String directoryName)400     private Uri acquireAccess(File file, String directoryName) {
401         StorageManager storageManager =
402                 (StorageManager) mActivity.getSystemService(Context.STORAGE_SERVICE);
403 
404         // Request access from DocumentsUI
405         final StorageVolume volume = storageManager.getStorageVolume(file);
406         final Intent intent = volume.createOpenDocumentTreeIntent();
407         mActivity.startActivityForResult(intent, REQUEST_CODE);
408 
409         if (mTargetPackageName == null) {
410             mTargetPackageName = getTargetPackageName(mActivity);
411         }
412 
413         // Granting the access
414         BySelector buttonPanelSelector = By.pkg(mTargetPackageName)
415                 .res(mTargetPackageName + ":id/container_save");
416         mDevice.wait(Until.hasObject(buttonPanelSelector), 30 * DateUtils.SECOND_IN_MILLIS);
417         final UiObject2 buttonPanel = mDevice.findObject(buttonPanelSelector);
418         final UiObject2 allowButton = buttonPanel.findObject(By.res("android:id/button1"));
419         allowButton.click();
420         mDevice.waitForIdle();
421 
422         // Granting the access by click "allow" in confirm dialog
423         final BySelector dialogButtonPanelSelector = By.pkg(mTargetPackageName)
424                 .res(mTargetPackageName + ":id/buttonPanel");
425         mDevice.wait(Until.hasObject(dialogButtonPanelSelector), 30 * DateUtils.SECOND_IN_MILLIS);
426         final UiObject2 positiveButton = mDevice.findObject(dialogButtonPanelSelector)
427                 .findObject(By.res("android:id/button1"));
428         positiveButton.click();
429         mDevice.waitForIdle();
430 
431         // Check granting result and take persistent permission
432         final Result result = mActivity.getResult();
433         assertEquals(Activity.RESULT_OK, result.resultCode);
434 
435         final Intent resultIntent = result.data;
436         final Uri resultUri = resultIntent.getData();
437         final int flags = resultIntent.getFlags()
438                 & (Intent.FLAG_GRANT_READ_URI_PERMISSION
439                     | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
440         mActivity.getContentResolver().takePersistableUriPermission(resultUri, flags);
441         return resultUri;
442     }
443 
getTargetPackageName(Context context)444     private static String getTargetPackageName(Context context) {
445         final PackageManager pm = context.getPackageManager();
446 
447         final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
448         intent.addCategory(Intent.CATEGORY_OPENABLE);
449         intent.setType("*/*");
450         final ResolveInfo ri = pm.resolveActivity(intent, 0);
451         return ri.activityInfo.packageName;
452     }
453 
454     // TODO: replace with ProviderTestUtils
executeShellCommand(String command)455     static String executeShellCommand(String command) throws IOException {
456         return executeShellCommand(command,
457                 InstrumentationRegistry.getInstrumentation().getUiAutomation());
458     }
459 
460     // TODO: replace with ProviderTestUtils
executeShellCommand(String command, UiAutomation uiAutomation)461     static String executeShellCommand(String command, UiAutomation uiAutomation)
462             throws IOException {
463         Log.v(TAG, "$ " + command);
464         ParcelFileDescriptor pfd = uiAutomation.executeShellCommand(command.toString());
465         BufferedReader br = null;
466         try (InputStream in = new FileInputStream(pfd.getFileDescriptor());) {
467             br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
468             String str = null;
469             StringBuilder out = new StringBuilder();
470             while ((str = br.readLine()) != null) {
471                 Log.v(TAG, "> " + str);
472                 out.append(str);
473             }
474             return out.toString();
475         } finally {
476             if (br != null) {
477                 br.close();
478             }
479         }
480     }
481 
482     // TODO: replace with ProviderTestUtils
stageFile(int resId, File file)483     static File stageFile(int resId, File file) throws IOException {
484         // The caller may be trying to stage into a location only available to
485         // the shell user, so we need to perform the entire copy as the shell
486         if (FileUtils.contains(Environment.getStorageDirectory(), file)) {
487             executeShellCommand("mkdir -p " + file.getParent());
488 
489             final Context context = InstrumentationRegistry.getTargetContext();
490             try (AssetFileDescriptor afd = context.getResources().openRawResourceFd(resId)) {
491                 final File source = ParcelFileDescriptor.getFile(afd.getFileDescriptor());
492                 final long skip = afd.getStartOffset();
493                 final long count = afd.getLength();
494 
495                 executeShellCommand(String.format("dd bs=1 if=%s skip=%d count=%d of=%s",
496                         source.getAbsolutePath(), skip, count, file.getAbsolutePath()));
497 
498                 // Force sync to try updating other views
499                 executeShellCommand("sync");
500             }
501         } else {
502             final File dir = file.getParentFile();
503             dir.mkdirs();
504             if (!dir.exists()) {
505                 throw new FileNotFoundException("Failed to create parent for " + file);
506             }
507             final Context context = InstrumentationRegistry.getTargetContext();
508             try (InputStream source = context.getResources().openRawResource(resId);
509                     OutputStream target = new FileOutputStream(file)) {
510                 FileUtils.copy(source, target);
511             }
512         }
513         return file;
514     }
515 }
516