blob: 6ce81a09af51cb751948d3d76345ce173f925d97 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.provider.cts;
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static android.Manifest.permission.CAMERA;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.Manifest.permission.RECORD_AUDIO;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.UriPermission;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.media.ExifInterface;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.os.SystemClock;
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
import android.provider.MediaStore;
import android.provider.cts.GetResultActivity.Result;
import android.support.test.InstrumentationRegistry;
import android.support.test.uiautomator.By;
import android.support.test.uiautomator.BySelector;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject2;
import android.support.test.uiautomator.UiSelector;
import android.support.test.uiautomator.Until;
import android.test.InstrumentationTestCase;
import android.text.format.DateUtils;
import android.util.Log;
import android.view.KeyEvent;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import androidx.core.content.FileProvider;
public class MediaStoreUiTest extends InstrumentationTestCase {
private static final String TAG = "MediaStoreUiTest";
private static final int REQUEST_CODE = 42;
private static final String CONTENT = "Test";
private UiDevice mDevice;
private GetResultActivity mActivity;
private File mFile;
private Uri mMediaStoreUri;
@Override
public void setUp() throws Exception {
mDevice = UiDevice.getInstance(getInstrumentation());
final Context context = getInstrumentation().getContext();
mActivity = launchActivity(context.getPackageName(), GetResultActivity.class, null);
mActivity.clearResult();
}
@Override
public void tearDown() throws Exception {
if (mFile != null) {
mFile.delete();
}
final ContentResolver resolver = mActivity.getContentResolver();
for (UriPermission permission : resolver.getPersistedUriPermissions()) {
mActivity.revokeUriPermission(
permission.getUri(),
Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
mActivity.finish();
}
public void testGetDocumentUri() throws Exception {
if (!supportsHardware()) return;
prepareFile();
final Uri treeUri = acquireAccess(mFile, Environment.DIRECTORY_DOCUMENTS);
assertNotNull(treeUri);
final Uri docUri = MediaStore.getDocumentUri(mActivity, mMediaStoreUri);
assertNotNull(docUri);
final ContentResolver resolver = mActivity.getContentResolver();
try (ParcelFileDescriptor fd = resolver.openFileDescriptor(docUri, "rw")) {
// Test reading
try (final BufferedReader reader =
new BufferedReader(new FileReader(fd.getFileDescriptor()))) {
assertEquals(CONTENT, reader.readLine());
}
// Test writing
try (final OutputStream out = new FileOutputStream(fd.getFileDescriptor())) {
out.write(CONTENT.getBytes());
}
}
}
public void testGetDocumentUri_ThrowsWithoutPermission() throws Exception {
if (!supportsHardware()) return;
prepareFile();
try {
MediaStore.getDocumentUri(mActivity, mMediaStoreUri);
fail("Expecting SecurityException.");
} catch (SecurityException e) {
// Expected
}
}
private void maybeClick(UiSelector sel) {
try { mDevice.findObject(sel).click(); } catch (Throwable ignored) { }
}
private void maybeClick(BySelector sel) {
try { mDevice.findObject(sel).click(); } catch (Throwable ignored) { }
}
private void maybeGrantRuntimePermission(String pkg, Set<String> requested, String permission) {
if (requested.contains(permission)) {
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.grantRuntimePermission(pkg, permission);
}
}
/**
* Verify that whoever handles {@link MediaStore#ACTION_IMAGE_CAPTURE} can
* correctly write the contents into a passed {@code content://} Uri.
*/
public void testImageCapture() throws Exception {
final Context context = getInstrumentation().getContext();
if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)) {
Log.d(TAG, "Skipping due to lack of camera");
return;
}
final File targetDir = new File(context.getFilesDir(), "debug");
final File target = new File(targetDir, "capture.jpg");
targetDir.mkdirs();
assertFalse(target.exists());
final Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT,
FileProvider.getUriForFile(context, "android.provider.cts.fileprovider", target));
// Figure out who is going to answer the phone
final ResolveInfo ri = context.getPackageManager().resolveActivity(intent, 0);
final String pkg = ri.activityInfo.packageName;
Log.d(TAG, "We're probably launching " + ri);
final PackageInfo pi = context.getPackageManager().getPackageInfo(pkg,
PackageManager.GET_PERMISSIONS);
final Set<String> req = new HashSet<>();
req.addAll(Arrays.asList(pi.requestedPermissions));
// Grant them all the permissions they might want
maybeGrantRuntimePermission(pkg, req, CAMERA);
maybeGrantRuntimePermission(pkg, req, ACCESS_COARSE_LOCATION);
maybeGrantRuntimePermission(pkg, req, ACCESS_FINE_LOCATION);
maybeGrantRuntimePermission(pkg, req, RECORD_AUDIO);
maybeGrantRuntimePermission(pkg, req, READ_EXTERNAL_STORAGE);
maybeGrantRuntimePermission(pkg, req, WRITE_EXTERNAL_STORAGE);
SystemClock.sleep(DateUtils.SECOND_IN_MILLIS);
mActivity.startActivityForResult(intent, REQUEST_CODE);
mDevice.waitForIdle();
// To ensure camera app is launched
SystemClock.sleep(5 * DateUtils.SECOND_IN_MILLIS);
// Try a couple different strategies for taking a photo: first take a
// photo and confirm using hardware keys
mDevice.pressKeyCode(KeyEvent.KEYCODE_CAMERA);
mDevice.waitForIdle();
SystemClock.sleep(5 * DateUtils.SECOND_IN_MILLIS);
mDevice.pressKeyCode(KeyEvent.KEYCODE_DPAD_CENTER);
mDevice.waitForIdle();
// Maybe that gave us a result?
Result result = mActivity.getResult(15, TimeUnit.SECONDS);
Log.d(TAG, "First pass result was " + result);
// Hrm, that didn't work; let's try an alternative approach of digging
// around for a shutter button
if (result == null) {
maybeClick(new UiSelector().resourceId(pkg + ":id/shutter_button"));
mDevice.waitForIdle();
SystemClock.sleep(5 * DateUtils.SECOND_IN_MILLIS);
maybeClick(new UiSelector().resourceId(pkg + ":id/shutter_button"));
mDevice.waitForIdle();
maybeClick(new UiSelector().resourceId(pkg + ":id/done_button"));
mDevice.waitForIdle();
result = mActivity.getResult(15, TimeUnit.SECONDS);
Log.d(TAG, "Second pass result was " + result);
}
// Grr, let's try hunting around even more
if (result == null) {
maybeClick(By.pkg(pkg).descContains("Capture"));
mDevice.waitForIdle();
SystemClock.sleep(5 * DateUtils.SECOND_IN_MILLIS);
maybeClick(By.pkg(pkg).descContains("Done"));
mDevice.waitForIdle();
result = mActivity.getResult(15, TimeUnit.SECONDS);
Log.d(TAG, "Third pass result was " + result);
}
assertNotNull("Expected to get a IMAGE_CAPTURE result; your camera app should "
+ "respond to the CAMERA and DPAD_CENTER keycodes", result);
assertTrue("exists", target.exists());
assertTrue("has data", target.length() > 65536);
// At the very least we expect photos generated by the device to have
// sane baseline EXIF data
final ExifInterface exif = new ExifInterface(new FileInputStream(target));
assertAttribute(exif, ExifInterface.TAG_MAKE);
assertAttribute(exif, ExifInterface.TAG_MODEL);
assertAttribute(exif, ExifInterface.TAG_DATETIME);
}
private static void assertAttribute(ExifInterface exif, String tag) {
final String res = exif.getAttribute(tag);
if (res == null || res.length() == 0) {
Log.d(TAG, "Expected valid EXIF tag for tag " + tag);
}
}
private boolean supportsHardware() {
final PackageManager pm = getInstrumentation().getContext().getPackageManager();
return !pm.hasSystemFeature("android.hardware.type.television")
&& !pm.hasSystemFeature("android.hardware.type.watch");
}
private void prepareFile() throws Exception {
assertEquals(Environment.MEDIA_MOUNTED, Environment.getExternalStorageState());
final File documents =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
documents.mkdirs();
assertTrue(documents.isDirectory());
mFile = new File(documents, "test.jpg");
try (OutputStream os = new FileOutputStream(mFile)) {
os.write(CONTENT.getBytes());
}
final CountDownLatch latch = new CountDownLatch(1);
MediaScannerConnection.scanFile(
mActivity,
new String[]{ mFile.getAbsolutePath() },
new String[]{ "image/jpeg" },
(String path, Uri uri) -> onScanCompleted(uri, latch)
);
assertTrue(
"MediaScanner didn't finish scanning in 30s.", latch.await(30, TimeUnit.SECONDS));
}
private void onScanCompleted(Uri uri, CountDownLatch latch) {
mMediaStoreUri = uri;
latch.countDown();
}
private Uri acquireAccess(File file, String directoryName) {
StorageManager storageManager =
(StorageManager) mActivity.getSystemService(Context.STORAGE_SERVICE);
// Request access from DocumentsUI
final StorageVolume volume = storageManager.getStorageVolume(file);
final Intent intent = volume.createAccessIntent(directoryName);
mActivity.startActivityForResult(intent, REQUEST_CODE);
// Granting the access
BySelector buttonPanelSelector = By.pkg("com.android.documentsui")
.res("android:id/buttonPanel");
mDevice.wait(Until.hasObject(buttonPanelSelector), 30 * DateUtils.SECOND_IN_MILLIS);
final UiObject2 buttonPanel = mDevice.findObject(buttonPanelSelector);
final UiObject2 allowButton = buttonPanel.findObject(By.res("android:id/button1"));
allowButton.click();
mDevice.waitForIdle();
// Check granting result and take persistent permission
final Result result = mActivity.getResult();
assertEquals(Activity.RESULT_OK, result.resultCode);
final Intent resultIntent = result.data;
final Uri resultUri = resultIntent.getData();
final int flags = resultIntent.getFlags()
& (Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
mActivity.getContentResolver().takePersistableUriPermission(resultUri, flags);
return resultUri;
}
}