blob: 36ad93290f4f64464d65c996d21c4ea666731bfd [file] [log] [blame]
/*
* Copyright (C) 2018 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 com.android.cts.mediastorageapp;
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.app.Activity;
import android.app.Instrumentation;
import android.app.PendingIntent;
import android.app.RecoverableSecurityException;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Environment;
import android.os.FileUtils;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
import android.provider.MediaStore.MediaColumns;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject;
import android.support.test.uiautomator.UiObjectNotFoundException;
import android.support.test.uiautomator.UiScrollable;
import android.support.test.uiautomator.UiSelector;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import com.android.cts.mediastorageapp.MediaStoreUtils.PendingParams;
import com.android.cts.mediastorageapp.MediaStoreUtils.PendingSession;
import com.google.common.io.ByteStreams;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.HashSet;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeoutException;
@RunWith(AndroidJUnit4.class)
public class MediaStorageTest {
private static final File TEST_JPG = Environment.buildPath(
Environment.getExternalStorageDirectory(),
Environment.DIRECTORY_DOWNLOADS, "mediastoragetest_file1.jpg");
private static final File TEST_PDF = Environment.buildPath(
Environment.getExternalStorageDirectory(),
Environment.DIRECTORY_DOWNLOADS, "mediastoragetest_file2.pdf");
private Context mContext;
private ContentResolver mContentResolver;
private int mUserId;
private static int currentAttempt = 0;
private static final int MAX_NUMBER_OF_ATTEMPT = 10;
@Before
public void setUp() throws Exception {
mContext = InstrumentationRegistry.getTargetContext();
mContentResolver = mContext.getContentResolver();
mUserId = mContext.getUserId();
}
@Test
public void testLegacy() throws Exception {
assertTrue(Environment.isExternalStorageLegacy());
// We can always see mounted state
assertEquals(Environment.MEDIA_MOUNTED, Environment.getExternalStorageState());
// We might have top-level access
final File probe = new File(Environment.getExternalStorageDirectory(),
"cts" + System.nanoTime());
assertTrue(probe.createNewFile());
assertNotNull(Environment.getExternalStorageDirectory().list());
// We always have our package directories
final File probePackage = new File(mContext.getExternalFilesDir(null),
"cts" + System.nanoTime());
assertTrue(probePackage.createNewFile());
assertTrue(TEST_JPG.exists());
assertTrue(TEST_PDF.exists());
final Uri jpgUri = MediaStore.scanFile(mContentResolver, TEST_JPG);
final Uri pdfUri = MediaStore.scanFile(mContentResolver, TEST_PDF);
final HashSet<Long> seen = new HashSet<>();
try (Cursor c = mContentResolver.query(
MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL),
new String[] { MediaColumns._ID }, null, null)) {
while (c.moveToNext()) {
seen.add(c.getLong(0));
}
}
assertTrue(seen.contains(ContentUris.parseId(jpgUri)));
assertTrue(seen.contains(ContentUris.parseId(pdfUri)));
}
@Test
public void testStageFiles() throws Exception {
final File jpg = stageFile(TEST_JPG);
assertTrue(jpg.exists());
final File pdf = stageFile(TEST_PDF);
assertTrue(pdf.exists());
}
@Test
public void testClearFiles() throws Exception {
TEST_JPG.delete();
assertNull(MediaStore.scanFile(mContentResolver, TEST_JPG));
TEST_PDF.delete();
assertNull(MediaStore.scanFile(mContentResolver, TEST_PDF));
}
@Test
public void testMediaNone() throws Exception {
doMediaNone(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MediaStorageTest::createAudio);
doMediaNone(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, MediaStorageTest::createVideo);
doMediaNone(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, MediaStorageTest::createImage);
// But since we don't hold the Music permission, we can't read the
// indexed metadata
try (Cursor c = mContentResolver.query(MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI,
null, null, null)) {
assertEquals(0, c.getCount());
}
try (Cursor c = mContentResolver.query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
null, null, null)) {
assertEquals(0, c.getCount());
}
try (Cursor c = mContentResolver.query(MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI,
null, null, null)) {
assertEquals(0, c.getCount());
}
}
private void doMediaNone(Uri collection, Callable<Uri> create) throws Exception {
final Uri red = create.call();
final Uri blue = create.call();
clearMediaOwner(blue, mUserId);
// Since we have no permissions, we should only be able to see media
// that we've contributed
final HashSet<Long> seen = new HashSet<>();
try (Cursor c = mContentResolver.query(collection,
new String[] { MediaColumns._ID }, null, null)) {
while (c.moveToNext()) {
seen.add(c.getLong(0));
}
}
assertTrue(seen.contains(ContentUris.parseId(red)));
assertFalse(seen.contains(ContentUris.parseId(blue)));
try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "rw")) {
}
try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(blue, "r")) {
fail("Expected read access to be blocked");
} catch (SecurityException | FileNotFoundException expected) {
}
try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(blue, "w")) {
fail("Expected write access to be blocked");
} catch (SecurityException | FileNotFoundException expected) {
}
// Verify that we can't grant ourselves access
for (int flag : new int[] {
Intent.FLAG_GRANT_READ_URI_PERMISSION,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
}) {
try {
mContext.grantUriPermission(mContext.getPackageName(), blue, flag);
fail("Expected granting to be blocked for flag 0x" + Integer.toHexString(flag));
} catch (SecurityException expected) {
}
}
}
/**
* Test prefix and non-prefix uri grant for all packages
*/
@Test
public void testGrantUriPermission() {
final int flagGrantRead = Intent.FLAG_GRANT_READ_URI_PERMISSION;
final int flagGrantWrite = Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
final int flagGrantReadPrefix =
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
final int flagGrantWritePrefix =
Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
for (Uri uri : new Uri[] {
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
}) {
// Non-prefix grant
checkGrantUriPermission(uri, flagGrantRead, true);
checkGrantUriPermission(uri, flagGrantWrite, true);
// Prefix grant
checkGrantUriPermission(uri, flagGrantReadPrefix, false);
checkGrantUriPermission(uri, flagGrantWritePrefix, false);
}
}
private void checkGrantUriPermission(Uri uri, int mode, boolean isGrantAllowed) {
if (isGrantAllowed) {
mContext.grantUriPermission(mContext.getPackageName(), uri, mode);
} else {
try {
mContext.grantUriPermission(mContext.getPackageName(), uri, mode);
fail("Expected granting to be blocked for flag 0x" + Integer.toHexString(mode));
} catch (SecurityException expected) {
}
}
}
@Test
public void testMediaRead() throws Exception {
doMediaRead(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MediaStorageTest::createAudio);
doMediaRead(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, MediaStorageTest::createVideo);
doMediaRead(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, MediaStorageTest::createImage);
}
private void doMediaRead(Uri collection, Callable<Uri> create) throws Exception {
final Uri red = create.call();
final Uri blue = create.call();
clearMediaOwner(blue, mUserId);
// Holding read permission we can see items we don't own
final HashSet<Long> seen = new HashSet<>();
try (Cursor c = mContentResolver.query(collection,
new String[] { MediaColumns._ID }, null, null)) {
while (c.moveToNext()) {
seen.add(c.getLong(0));
}
}
assertTrue(seen.contains(ContentUris.parseId(red)));
assertTrue(seen.contains(ContentUris.parseId(blue)));
try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "rw")) {
}
try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(blue, "r")) {
}
try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(blue, "w")) {
fail("Expected write access to be blocked");
} catch (SecurityException | FileNotFoundException expected) {
}
}
@Test
public void testMediaWrite() throws Exception {
doMediaWrite(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MediaStorageTest::createAudio);
doMediaWrite(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, MediaStorageTest::createVideo);
doMediaWrite(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, MediaStorageTest::createImage);
}
private void doMediaWrite(Uri collection, Callable<Uri> create) throws Exception {
final Uri red = create.call();
final Uri blue = create.call();
clearMediaOwner(blue, mUserId);
// Holding read permission we can see items we don't own
final HashSet<Long> seen = new HashSet<>();
try (Cursor c = mContentResolver.query(collection,
new String[] { MediaColumns._ID }, null, null)) {
while (c.moveToNext()) {
seen.add(c.getLong(0));
}
}
assertTrue(seen.contains(ContentUris.parseId(red)));
assertTrue(seen.contains(ContentUris.parseId(blue)));
try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "rw")) {
}
try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(blue, "r")) {
}
if (Environment.isExternalStorageLegacy()) {
try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(blue, "w")) {
}
} else {
try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(blue, "w")) {
fail("Expected write access to be blocked");
} catch (SecurityException | FileNotFoundException expected) {
}
}
}
@Test
public void testMediaEscalation_Open() throws Exception {
doMediaEscalation_Open(MediaStorageTest::createAudio);
doMediaEscalation_Open(MediaStorageTest::createVideo);
doMediaEscalation_Open(MediaStorageTest::createImage);
}
private void doMediaEscalation_Open(Callable<Uri> create) throws Exception {
final Uri red = create.call();
clearMediaOwner(red, mUserId);
RecoverableSecurityException exception = null;
try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "w")) {
fail("Expected write access to be blocked");
} catch (RecoverableSecurityException expected) {
exception = expected;
}
doEscalation(exception);
try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "w")) {
}
}
@Test
public void testMediaEscalation_Update() throws Exception {
doMediaEscalation_Update(MediaStorageTest::createAudio);
doMediaEscalation_Update(MediaStorageTest::createVideo);
doMediaEscalation_Update(MediaStorageTest::createImage);
}
private void doMediaEscalation_Update(Callable<Uri> create) throws Exception {
final Uri red = create.call();
clearMediaOwner(red, mUserId);
final ContentValues values = new ContentValues();
values.put(MediaColumns.DISPLAY_NAME, "cts" + System.nanoTime());
RecoverableSecurityException exception = null;
try {
mContentResolver.update(red, values, null, null);
fail("Expected update access to be blocked");
} catch (RecoverableSecurityException expected) {
exception = expected;
}
doEscalation(exception);
assertEquals(1, mContentResolver.update(red, values, null, null));
}
@Test
public void testMediaEscalation_Delete() throws Exception {
doMediaEscalation_Delete(MediaStorageTest::createAudio);
doMediaEscalation_Delete(MediaStorageTest::createVideo);
doMediaEscalation_Delete(MediaStorageTest::createImage);
}
private void doMediaEscalation_Delete(Callable<Uri> create) throws Exception {
final Uri red = create.call();
clearMediaOwner(red, mUserId);
RecoverableSecurityException exception = null;
try {
mContentResolver.delete(red, null, null);
fail("Expected update access to be blocked");
} catch (RecoverableSecurityException expected) {
exception = expected;
}
doEscalation(exception);
assertEquals(1, mContentResolver.delete(red, null, null));
}
@Test
public void testMediaEscalation_RequestWriteFilePathSupport() throws Exception {
doMediaEscalation_RequestWrite_withFilePathSupport(MediaStorageTest::createAudio);
doMediaEscalation_RequestWrite_withFilePathSupport(MediaStorageTest::createVideo);
doMediaEscalation_RequestWrite_withFilePathSupport(MediaStorageTest::createImage);
doMediaEscalation_RequestWrite_withFilePathSupport(MediaStorageTest::createPlaylist);
doMediaEscalation_RequestWrite_withFilePathSupport(MediaStorageTest::createSubtitle);
}
private void doMediaEscalation_RequestWrite_withFilePathSupport(
Callable<Uri> create) throws Exception {
final Uri red = create.call();
assertNotNull(red);
String path = queryForSingleColumn(red, MediaColumns.DATA);
File file = new File(path);
assertThat(file.exists()).isTrue();
assertThat(file.canRead()).isTrue();
assertThat(file.canWrite()).isTrue();
clearMediaOwner(red, mUserId);
assertThat(file.canWrite()).isFalse();
try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "w")) {
fail("Expected write access to be blocked");
} catch (SecurityException expected) {
}
doEscalation(MediaStore.createWriteRequest(mContentResolver, Arrays.asList(red)));
try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "w")) {
}
// Wait for MediaStore to be idle to avoid flakiness due to race conditions between
// MediaStore.scanFile (which is called above in #openFileDescriptor) and rename (which is
// called below). This is a known issue: b/158982091
MediaStore.waitForIdle(mContentResolver);
// Check File API support
assertAccessFileAPISupport(file);
assertReadWriteFileAPISupport(file);
assertRenameFileAPISupport(file);
assertDeleteFileAPISupport(file);
}
private void assertAccessFileAPISupport(File file) throws Exception {
assertThat(file.canRead()).isTrue();
assertThat(file.canWrite()).isTrue();
}
private void assertReadWriteFileAPISupport(File file) throws Exception {
final String str = "Just some random text";
final byte[] bytes = str.getBytes();
// Write to file
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(bytes);
}
// Read the same data from file
try (FileInputStream fis = new FileInputStream(file)) {
assertThat(ByteStreams.toByteArray(fis)).isEqualTo(bytes);
}
}
public void assertRenameFileAPISupport(File oldFile) throws Exception {
final String oldName = oldFile.getAbsolutePath();
final String extension = oldName.substring(oldName.lastIndexOf('.')).trim();
// TODO(b/178816495): Changing the extension changes the media-type and hence the media-URI
// corresponding to the new file is not accessible to the caller. Rename to the same
// extension so that the test app does not lose access and is able to delete the file.
final String newName = "cts" + System.nanoTime() + extension;
final File newFile = Environment.buildPath(Environment.getExternalStorageDirectory(),
Environment.DIRECTORY_DOWNLOADS, newName);
assertThat(oldFile.renameTo(newFile)).isTrue();
// Rename back to oldFile for other ops like delete
assertThat(newFile.renameTo(oldFile)).isTrue();
}
private void assertDeleteFileAPISupport(File file) throws Exception {
assertThat(file.delete()).isTrue();
}
@Test
public void testMediaEscalation_RequestWrite() throws Exception {
doMediaEscalation_RequestWrite(true /* allowAccess */,
false /* shouldCheckDialogShownValue */, false /* isDialogShownExpected */);
}
@Test
public void testMediaEscalationWithDenied_RequestWrite() throws Exception {
doMediaEscalation_RequestWrite(false /* allowAccess */,
false /* shouldCheckDialogShownValue */, false /* isDialogShownExpected */);
}
@Test
public void testMediaEscalation_RequestWrite_showConfirmDialog() throws Exception {
doMediaEscalation_RequestWrite(true /* allowAccess */,
true /* shouldCheckDialogShownValue */, true /* isDialogShownExpected */);
}
@Test
public void testMediaEscalation_RequestWrite_notShowConfirmDialog() throws Exception {
doMediaEscalation_RequestWrite(true /* allowAccess */,
true /* shouldCheckDialogShownValue */, false /* isDialogShownExpected */);
}
private void doMediaEscalation_RequestWrite(boolean allowAccess,
boolean shouldCheckDialogShownValue, boolean isDialogShownExpected) throws Exception {
doMediaEscalation_RequestWrite(MediaStorageTest::createAudio, allowAccess,
shouldCheckDialogShownValue, isDialogShownExpected);
doMediaEscalation_RequestWrite(MediaStorageTest::createVideo, allowAccess,
shouldCheckDialogShownValue, isDialogShownExpected);
doMediaEscalation_RequestWrite(MediaStorageTest::createImage, allowAccess,
shouldCheckDialogShownValue, isDialogShownExpected);
doMediaEscalation_RequestWrite(MediaStorageTest::createPlaylist, allowAccess,
shouldCheckDialogShownValue, isDialogShownExpected);
doMediaEscalation_RequestWrite(MediaStorageTest::createSubtitle, allowAccess,
shouldCheckDialogShownValue, isDialogShownExpected);
}
private void doMediaEscalation_RequestWrite(Callable<Uri> create, boolean allowAccess,
boolean shouldCheckDialogShownValue, boolean isDialogShownExpected) throws Exception {
final Uri red = create.call();
clearMediaOwner(red, mUserId);
try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "w")) {
fail("Expected write access to be blocked");
} catch (SecurityException expected) {
}
if (allowAccess) {
doEscalation(MediaStore.createWriteRequest(mContentResolver, Arrays.asList(red)),
true /* allowAccess */, shouldCheckDialogShownValue, isDialogShownExpected);
try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "w")) {
}
} else {
doEscalation(MediaStore.createWriteRequest(mContentResolver, Arrays.asList(red)),
false /* allowAccess */, shouldCheckDialogShownValue, isDialogShownExpected);
try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "w")) {
fail("Expected write access to be blocked");
} catch (SecurityException expected) {
}
}
}
@Test
public void testMediaEscalationWithDenied_RequestUnTrash() throws Exception {
doMediaEscalationWithDenied_RequestUnTrash(MediaStorageTest::createAudio);
doMediaEscalationWithDenied_RequestUnTrash(MediaStorageTest::createVideo);
doMediaEscalationWithDenied_RequestUnTrash(MediaStorageTest::createImage);
doMediaEscalationWithDenied_RequestUnTrash(MediaStorageTest::createPlaylist);
doMediaEscalationWithDenied_RequestUnTrash(MediaStorageTest::createSubtitle);
}
private void doMediaEscalationWithDenied_RequestUnTrash(Callable<Uri> create) throws Exception {
final Uri red = create.call();
clearMediaOwner(red, mUserId);
assertEquals("0", queryForSingleColumn(red, MediaColumns.IS_TRASHED));
doEscalation(
MediaStore.createTrashRequest(mContentResolver, Arrays.asList(red), true));
assertEquals("1", queryForSingleColumn(red, MediaColumns.IS_TRASHED));
doEscalation(MediaStore.createTrashRequest(mContentResolver, Arrays.asList(red), false),
false /* allowAccess */, false /* shouldCheckDialogShownValue */,
false /* isDialogShownExpected */);
assertEquals("1", queryForSingleColumn(red, MediaColumns.IS_TRASHED));
}
@Test
public void testMediaEscalation_RequestTrash() throws Exception {
doMediaEscalation_RequestTrash(true /* allowAccess */,
false /* shouldCheckDialogShownValue */, false /* isDialogShownExpected */);
}
@Test
public void testMediaEscalationWithDenied_RequestTrash() throws Exception {
doMediaEscalation_RequestTrash(false /* allowAccess */,
false /* shouldCheckDialogShownValue */, false /* isDialogShownExpected */);
}
@Test
public void testMediaEscalation_RequestTrash_showConfirmDialog() throws Exception {
doMediaEscalation_RequestTrash(true /* allowAccess */,
true /* shouldCheckDialogShownValue */, true /* isDialogShownExpected */);
}
@Test
public void testMediaEscalation_RequestTrash_notShowConfirmDialog() throws Exception {
doMediaEscalation_RequestTrash(true /* allowAccess */,
true /* shouldCheckDialogShownValue */, false /* isDialogShownExpected */);
}
private void doMediaEscalation_RequestTrash(boolean allowAccess,
boolean shouldCheckDialogShownValue, boolean isDialogShownExpected) throws Exception {
doMediaEscalation_RequestTrash(MediaStorageTest::createAudio, allowAccess,
shouldCheckDialogShownValue, isDialogShownExpected);
doMediaEscalation_RequestTrash(MediaStorageTest::createVideo, allowAccess,
shouldCheckDialogShownValue, isDialogShownExpected);
doMediaEscalation_RequestTrash(MediaStorageTest::createImage, allowAccess,
shouldCheckDialogShownValue, isDialogShownExpected);
doMediaEscalation_RequestTrash(MediaStorageTest::createPlaylist, allowAccess,
shouldCheckDialogShownValue, isDialogShownExpected);
doMediaEscalation_RequestTrash(MediaStorageTest::createSubtitle, allowAccess,
shouldCheckDialogShownValue, isDialogShownExpected);
}
private void doMediaEscalation_RequestTrash(Callable<Uri> create, boolean allowAccess,
boolean shouldCheckDialogShownValue, boolean isDialogShownExpected) throws Exception {
final Uri red = create.call();
clearMediaOwner(red, mUserId);
assertEquals("0", queryForSingleColumn(red, MediaColumns.IS_TRASHED));
if (allowAccess) {
doEscalation(MediaStore.createTrashRequest(mContentResolver, Arrays.asList(red), true),
true /* allowAccess */, shouldCheckDialogShownValue, isDialogShownExpected);
assertEquals("1", queryForSingleColumn(red, MediaColumns.IS_TRASHED));
doEscalation(MediaStore.createTrashRequest(mContentResolver, Arrays.asList(red), false),
true /* allowAccess */, shouldCheckDialogShownValue, isDialogShownExpected);
assertEquals("0", queryForSingleColumn(red, MediaColumns.IS_TRASHED));
} else {
doEscalation(MediaStore.createTrashRequest(mContentResolver, Arrays.asList(red), true),
false /* allowAccess */, shouldCheckDialogShownValue, isDialogShownExpected);
assertEquals("0", queryForSingleColumn(red, MediaColumns.IS_TRASHED));
}
}
@Test
public void testMediaEscalation_RequestFavorite() throws Exception {
doMediaEscalation_RequestFavorite(MediaStorageTest::createAudio);
doMediaEscalation_RequestFavorite(MediaStorageTest::createVideo);
doMediaEscalation_RequestFavorite(MediaStorageTest::createImage);
doMediaEscalation_RequestFavorite(MediaStorageTest::createPlaylist);
doMediaEscalation_RequestFavorite(MediaStorageTest::createSubtitle);
}
private void doMediaEscalation_RequestFavorite(Callable<Uri> create) throws Exception {
final Uri red = create.call();
clearMediaOwner(red, mUserId);
assertEquals("0", queryForSingleColumn(red, MediaColumns.IS_FAVORITE));
doEscalation(MediaStore.createFavoriteRequest(mContentResolver, Arrays.asList(red), true));
assertEquals("1", queryForSingleColumn(red, MediaColumns.IS_FAVORITE));
doEscalation(MediaStore.createFavoriteRequest(mContentResolver, Arrays.asList(red), false));
assertEquals("0", queryForSingleColumn(red, MediaColumns.IS_FAVORITE));
}
@Test
public void testMediaEscalation_RequestDelete() throws Exception {
doMediaEscalation_RequestDelete(true /* allowAccess */,
false /* shouldCheckDialogShownValue */, false /* isDialogShownExpected */);
}
@Test
public void testMediaEscalationWithDenied_RequestDelete() throws Exception {
doMediaEscalation_RequestDelete(false /* allowAccess */,
false /* shouldCheckDialogShownValue */, false /* isDialogShownExpected */);
}
@Test
public void testMediaEscalation_RequestDelete_showConfirmDialog() throws Exception {
doMediaEscalation_RequestDelete(true /* allowAccess */,
true /* shouldCheckDialogShownValue */, true /* isDialogShownExpected */);
}
@Test
public void testMediaEscalation_RequestDelete_notShowConfirmDialog() throws Exception {
doMediaEscalation_RequestDelete(true /* allowAccess */,
true /* shouldCheckDialogShownValue */, false /* isDialogShownExpected */);
}
private void doMediaEscalation_RequestDelete(boolean allowAccess,
boolean shouldCheckDialogShownValue, boolean isDialogShownExpected) throws Exception {
doMediaEscalation_RequestDelete(MediaStorageTest::createAudio, allowAccess,
shouldCheckDialogShownValue, isDialogShownExpected);
doMediaEscalation_RequestDelete(MediaStorageTest::createVideo, allowAccess,
shouldCheckDialogShownValue, isDialogShownExpected);
doMediaEscalation_RequestDelete(MediaStorageTest::createImage, allowAccess,
shouldCheckDialogShownValue, isDialogShownExpected);
doMediaEscalation_RequestDelete(MediaStorageTest::createPlaylist, allowAccess,
shouldCheckDialogShownValue, isDialogShownExpected);
doMediaEscalation_RequestDelete(MediaStorageTest::createSubtitle, allowAccess,
shouldCheckDialogShownValue, isDialogShownExpected);
}
private void doMediaEscalation_RequestDelete(Callable<Uri> create, boolean allowAccess,
boolean shouldCheckDialogShownValue, boolean isDialogShownExpected) throws Exception {
final Uri red = create.call();
clearMediaOwner(red, mUserId);
try (Cursor c = mContentResolver.query(red, null, null, null)) {
assertEquals(1, c.getCount());
}
if (allowAccess) {
doEscalation(MediaStore.createDeleteRequest(mContentResolver, Arrays.asList(red)),
true /* allowAccess */, shouldCheckDialogShownValue, isDialogShownExpected);
try (Cursor c = mContentResolver.query(red, null, null, null)) {
assertEquals(0, c.getCount());
}
} else {
doEscalation(MediaStore.createDeleteRequest(mContentResolver, Arrays.asList(red)),
false /* allowAccess */, shouldCheckDialogShownValue, isDialogShownExpected);
try (Cursor c = mContentResolver.query(red, null, null, null)) {
assertEquals(1, c.getCount());
}
}
}
private void doEscalation(RecoverableSecurityException exception) throws Exception {
doEscalation(exception.getUserAction().getActionIntent());
}
private void doEscalation(PendingIntent pi) throws Exception {
doEscalation(pi, true /* allowAccess */, false /* shouldCheckDialogShownValue */,
false /* isDialogShownExpectedExpected */);
}
private void doEscalation(PendingIntent pi, boolean allowAccess,
boolean shouldCheckDialogShownValue, boolean isDialogShownExpected) throws Exception {
// Try launching the action to grant ourselves access
final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
final Intent intent = new Intent(inst.getContext(), GetResultActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// Wake up the device and dismiss the keyguard before the test starts
final UiDevice device = UiDevice.getInstance(inst);
device.executeShellCommand("input keyevent KEYCODE_WAKEUP");
device.executeShellCommand("wm dismiss-keyguard");
final GetResultActivity activity = (GetResultActivity) inst.startActivitySync(intent);
// Wait for the UI Thread to become idle.
inst.waitForIdleSync();
activity.clearResult();
device.waitForIdle();
activity.startIntentSenderForResult(pi.getIntentSender(), 42, null, 0, 0, 0);
device.waitForIdle();
final long timeout = 5_000;
if (allowAccess) {
// Some dialogs may have granted access automatically, so we're willing
// to keep rolling forward if we can't find our grant button
final UiSelector grant = new UiSelector().textMatches("(?i)Allow");
if (isWatch()) {
UiScrollable uiScrollable = new UiScrollable(new UiSelector().scrollable(true));
try {
uiScrollable.scrollIntoView(grant);
} catch (UiObjectNotFoundException e) {
// Scrolling can fail if the UI is not scrollable
}
}
final boolean grantExists = new UiObject(grant).waitForExists(timeout);
if (shouldCheckDialogShownValue) {
assertThat(grantExists).isEqualTo(isDialogShownExpected);
}
if (grantExists) {
device.findObject(grant).click();
}
final GetResultActivity.Result res = activity.getResult();
// Verify that we now have access
assertEquals(Activity.RESULT_OK, res.resultCode);
} else {
// fine the Deny button
final UiSelector deny = new UiSelector().textMatches("(?i)Deny");
final boolean denyExists = new UiObject(deny).waitForExists(timeout);
assertThat(denyExists).isTrue();
device.findObject(deny).click();
final GetResultActivity.Result res = activity.getResult();
// Verify that we don't have access
assertEquals(Activity.RESULT_CANCELED, res.resultCode);
}
}
private static Uri createAudio() throws IOException {
final Context context = InstrumentationRegistry.getTargetContext();
final String displayName = "cts" + System.nanoTime();
final PendingParams params = new PendingParams(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, displayName, "audio/mpeg");
final Uri pendingUri = MediaStoreUtils.createPending(context, params);
try (PendingSession session = MediaStoreUtils.openPending(context, pendingUri)) {
try (InputStream in = context.getResources().getAssets().open("testmp3.mp3");
OutputStream out = session.openOutputStream()) {
FileUtils.copy(in, out);
}
return session.publish();
}
}
private static Uri createVideo() throws IOException {
final Context context = InstrumentationRegistry.getTargetContext();
final String displayName = "cts" + System.nanoTime();
final PendingParams params = new PendingParams(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI, displayName, "video/mpeg");
final Uri pendingUri = MediaStoreUtils.createPending(context, params);
try (PendingSession session = MediaStoreUtils.openPending(context, pendingUri)) {
try (InputStream in = context.getResources().getAssets().open("testmp3.mp3");
OutputStream out = session.openOutputStream()) {
FileUtils.copy(in, out);
}
return session.publish();
}
}
private static Uri createImage() throws IOException {
final Context context = InstrumentationRegistry.getTargetContext();
final String displayName = "cts" + System.nanoTime();
final PendingParams params = new PendingParams(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, displayName, "image/png");
final Uri pendingUri = MediaStoreUtils.createPending(context, params);
try (PendingSession session = MediaStoreUtils.openPending(context, pendingUri)) {
try (OutputStream out = session.openOutputStream()) {
final Bitmap bitmap = Bitmap.createBitmap(32, 32, Bitmap.Config.ARGB_8888);
bitmap.compress(Bitmap.CompressFormat.PNG, 90, out);
}
return session.publish();
}
}
private static Uri createPlaylist() throws IOException {
final Context context = InstrumentationRegistry.getTargetContext();
final String displayName = "cts" + System.nanoTime();
final PendingParams params = new PendingParams(
MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, displayName, "audio/mpegurl");
final Uri pendingUri = MediaStoreUtils.createPending(context, params);
try (PendingSession session = MediaStoreUtils.openPending(context, pendingUri)) {
return session.publish();
}
}
private static Uri createSubtitle() throws IOException {
final Context context = InstrumentationRegistry.getTargetContext();
final String displayName = "cts" + System.nanoTime();
final PendingParams params = new PendingParams(
MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL), displayName,
"application/x-subrip");
final Uri pendingUri = MediaStoreUtils.createPending(context, params);
try (PendingSession session = MediaStoreUtils.openPending(context, pendingUri)) {
try (InputStream in = context.getResources().getAssets().open("testmp3.mp3");
OutputStream out = session.openOutputStream()) {
FileUtils.copy(in, out);
}
return session.publish();
}
}
private static String queryForSingleColumn(Uri uri, String column) throws Exception {
final ContentResolver resolver = InstrumentationRegistry.getTargetContext()
.getContentResolver();
try (Cursor c = resolver.query(uri, new String[] { column }, null, null)) {
assertEquals(1, c.getCount());
assertTrue(c.moveToFirst());
return c.getString(0);
}
}
private static void clearMediaOwner(Uri uri, int userId) throws IOException {
final String cmd = String.format(
"content update --uri %s --user %d --bind owner_package_name:n:",
uri, userId);
runShellCommand(InstrumentationRegistry.getInstrumentation(), cmd);
}
static File stageFile(File file) throws Exception {
// Sometimes file creation fails due to slow permission update, try more times
while(currentAttempt < MAX_NUMBER_OF_ATTEMPT) {
try {
file.getParentFile().mkdirs();
file.createNewFile();
return file;
} catch(IOException e) {
currentAttempt++;
// wait 500ms
Thread.sleep(500);
}
}
throw new TimeoutException("File creation failed due to slow permission update");
}
private boolean isWatch() {
return hasFeature(PackageManager.FEATURE_WATCH);
}
private boolean hasFeature(String feature) {
return mContext.getPackageManager().hasSystemFeature(feature);
}
}