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