blob: b23f945ae4da801efdce17f02bb44a9104182099 [file] [log] [blame]
/*
* Copyright (C) 2021 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.scopedstorage.cts.device;
import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
import static android.database.Cursor.FIELD_TYPE_BLOB;
import static android.scopedstorage.cts.lib.TestUtils.assertThrows;
import static android.scopedstorage.cts.lib.TestUtils.canOpenRedactedUriForWrite;
import static android.scopedstorage.cts.lib.TestUtils.canQueryOnUri;
import static android.scopedstorage.cts.lib.TestUtils.checkPermission;
import static android.scopedstorage.cts.lib.TestUtils.forceStopApp;
import static android.scopedstorage.cts.lib.TestUtils.getContentResolver;
import static android.scopedstorage.cts.lib.TestUtils.grantPermission;
import static android.scopedstorage.cts.lib.TestUtils.installAppWithStoragePermissions;
import static android.scopedstorage.cts.lib.TestUtils.isFileDescriptorRedacted;
import static android.scopedstorage.cts.lib.TestUtils.isFileOpenRedacted;
import static android.scopedstorage.cts.lib.TestUtils.setShouldForceStopTestApp;
import static android.scopedstorage.cts.lib.TestUtils.uninstallAppNoThrow;
import static androidx.test.InstrumentationRegistry.getContext;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import android.Manifest;
import android.content.ContentValues;
import android.content.Intent;
import android.database.Cursor;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.FileUtils;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
import androidx.test.filters.SdkSuppress;
import com.android.cts.install.lib.TestApp;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
/**
* Device-side test suite to verify redacted URI operations.
*/
@RunWith(Parameterized.class)
@SdkSuppress(minSdkVersion = 31, codeName = "S")
public class RedactUriDeviceTest extends ScopedStorageBaseDeviceTest {
/**
* To help avoid flaky tests, give ourselves a unique nonce to be used for
* all filesystem paths, so that we don't risk conflicting with previous
* test runs.
*/
static final String NONCE = String.valueOf(System.nanoTime());
static final String IMAGE_FILE_NAME = "ScopedStorageDeviceTest_file_" + NONCE + ".jpg";
// An app with no permissions
private static final TestApp APP_B_NO_PERMS = new TestApp("TestAppB",
"android.scopedstorage.cts.testapp.B.noperms", 1, false,
"CtsScopedStorageTestAppB.apk");
// The following apps are not installed at test startup - please install before using.
private static final TestApp APP_C = new TestApp("TestAppC",
"android.scopedstorage.cts.testapp.C", 1, false, "CtsScopedStorageTestAppC.apk");
@Parameterized.Parameter(0)
public String mVolumeName;
/** Parameters data. */
@Parameterized.Parameters(name = "volume={0}")
public static Iterable<? extends Object> data() {
return getTestParameters();
}
@BeforeClass
public static void setupApps() {
// Installed by target preparer
assertThat(checkPermission(APP_B_NO_PERMS,
Manifest.permission.READ_EXTERNAL_STORAGE)).isFalse();
setShouldForceStopTestApp(false);
}
@AfterClass
public static void destroy() {
setShouldForceStopTestApp(true);
}
@Test
public void testRedactedUri_single() throws Exception {
final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
try {
final Uri uri = MediaStore.scanFile(getContentResolver(), img);
final Uri redactedUri = MediaStore.getRedactedUri(getContentResolver(), uri);
testRedactedUriCommon(uri, redactedUri);
} finally {
img.delete();
}
}
@Test
public void testRedactedUri_list() throws Exception {
List<Uri> uris = new ArrayList<>();
List<File> files = new ArrayList<>();
try {
for (int i = 0; i < 10; i++) {
File file = stageImageFileWithMetadata("img_metadata" + String.valueOf(
System.nanoTime()) + i + ".jpg");
files.add(file);
uris.add(MediaStore.scanFile(getContentResolver(), file));
}
final Collection<Uri> redactedUris = MediaStore.getRedactedUri(getContentResolver(),
uris);
int i = 0;
for (Uri redactedUri : redactedUris) {
Uri uri = uris.get(i++);
testRedactedUriCommon(uri, redactedUri);
}
} finally {
files.forEach(file -> file.delete());
}
}
@Test
public void testQueryOnRedactionUri() throws Exception {
final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
final Uri uri = MediaStore.scanFile(getContentResolver(), img);
final Uri redactedUri = MediaStore.getRedactedUri(getContentResolver(), uri);
final Cursor uriCursor = getContentResolver().query(uri, null, null, null);
final String redactedUriDir = ".transforms/synthetic/redacted";
final String redactedUriDirAbsolutePath =
Environment.getExternalStorageDirectory() + "/" + redactedUriDir;
try {
assertNotNull(uriCursor);
assertThat(uriCursor.moveToFirst()).isTrue();
final Cursor redactedUriCursor = getContentResolver().query(redactedUri, null, null,
null);
assertNotNull(redactedUriCursor);
assertThat(redactedUriCursor.moveToFirst()).isTrue();
assertEquals(redactedUriCursor.getColumnCount(), uriCursor.getColumnCount());
final String data = getStringFromCursor(redactedUriCursor,
MediaStore.MediaColumns.DATA);
final String redactedUriDisplayName = redactedUri.getLastPathSegment() + ".jpg";
assertEquals(redactedUriDirAbsolutePath + "/" + redactedUriDisplayName, data);
final String name = getStringFromCursor(redactedUriCursor,
MediaStore.MediaColumns.DISPLAY_NAME);
assertEquals(redactedUriDisplayName, name);
final String relativePath = getStringFromCursor(redactedUriCursor,
MediaStore.MediaColumns.RELATIVE_PATH);
assertEquals(redactedUriDir, relativePath);
final String bucketDisplayName = getStringFromCursor(redactedUriCursor,
MediaStore.MediaColumns.BUCKET_DISPLAY_NAME);
assertEquals(redactedUriDir, bucketDisplayName);
final String docId = getStringFromCursor(redactedUriCursor,
MediaStore.MediaColumns.DOCUMENT_ID);
assertNull(docId);
final String insId = getStringFromCursor(redactedUriCursor,
MediaStore.MediaColumns.INSTANCE_ID);
assertNull(insId);
final String bucId = getStringFromCursor(redactedUriCursor,
MediaStore.MediaColumns.BUCKET_ID);
assertNull(bucId);
final Collection<String> updatedCols = Arrays.asList(MediaStore.MediaColumns._ID,
MediaStore.MediaColumns.DISPLAY_NAME,
MediaStore.MediaColumns.RELATIVE_PATH,
MediaStore.MediaColumns.BUCKET_DISPLAY_NAME,
MediaStore.MediaColumns.DATA,
MediaStore.MediaColumns.DOCUMENT_ID,
MediaStore.MediaColumns.INSTANCE_ID,
MediaStore.MediaColumns.BUCKET_ID);
for (String colName : uriCursor.getColumnNames()) {
if (!updatedCols.contains(colName)) {
if (uriCursor.getType(uriCursor.getColumnIndex(colName)) == FIELD_TYPE_BLOB) {
assertThat(
Arrays.equals(uriCursor.getBlob(uriCursor.getColumnIndex(colName)),
redactedUriCursor.getBlob(redactedUriCursor.getColumnIndex(
colName)))).isTrue();
} else {
assertEquals(getStringFromCursor(uriCursor, colName),
getStringFromCursor(redactedUriCursor, colName));
}
}
}
} finally {
img.delete();
}
}
/*
* Verify that app can't open the shared redacted URI for write.
**/
@Test
public void testSharedRedactedUri_openFdForWrite() throws Exception {
forceStopApp(APP_B_NO_PERMS.getPackageName());
final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
try {
Uri redactedUri = shareAndGetRedactedUri(img, APP_B_NO_PERMS);
assertThrows(UnsupportedOperationException.class,
() -> canOpenRedactedUriForWrite(APP_B_NO_PERMS, redactedUri));
} finally {
img.delete();
}
}
/*
* Verify that app with correct permission can open the shared redacted URI for read in
* redacted mode.
**/
@Test
public void testSharedRedactedUri_openFdForRead() throws Exception {
forceStopApp(APP_B_NO_PERMS.getPackageName());
final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
try {
final Uri redactedUri = shareAndGetRedactedUri(img, APP_B_NO_PERMS);
assertThat(isFileDescriptorRedacted(APP_B_NO_PERMS, redactedUri)).isTrue();
} finally {
img.delete();
}
}
/*
* Verify that app with correct permission can open the shared redacted URI for read in
* redacted mode.
**/
@Test
public void testSharedRedactedUri_openFileForRead() throws Exception {
forceStopApp(APP_B_NO_PERMS.getPackageName());
final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
try {
Uri redactedUri = shareAndGetRedactedUri(img, APP_B_NO_PERMS);
assertThat(isFileOpenRedacted(APP_B_NO_PERMS, redactedUri)).isTrue();
} finally {
img.delete();
}
}
/*
* Verify that the app with redacted URI granted can query it.
**/
@Test
public void testSharedRedactedUri_query() throws Exception {
forceStopApp(APP_B_NO_PERMS.getPackageName());
final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
try {
Uri redactedUri = shareAndGetRedactedUri(img, APP_B_NO_PERMS);
assertThat(canQueryOnUri(APP_B_NO_PERMS, redactedUri)).isTrue();
} finally {
img.delete();
}
}
/*
* Verify that for app with AML permission shared redacted URI opens for read in redacted mode.
**/
@Test
public void testSharedRedactedUri_openFileForRead_withLocationPerm() throws Exception {
final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
try {
// Install test app
installAppWithStoragePermissions(APP_C);
grantPermission(APP_C.getPackageName(), Manifest.permission.ACCESS_MEDIA_LOCATION);
Uri redactedUri = shareAndGetRedactedUri(img, APP_C);
assertThat(isFileOpenRedacted(APP_C, redactedUri)).isTrue();
} finally {
img.delete();
uninstallAppNoThrow(APP_C);
}
}
/*
* Verify that for app with AML permission shared redacted URI opens for read in redacted mode.
**/
@Test
public void testSharedRedactedUri_openFdForRead_withLocationPerm() throws Exception {
final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
try {
// Install test app
installAppWithStoragePermissions(APP_C);
grantPermission(APP_C.getPackageName(), Manifest.permission.ACCESS_MEDIA_LOCATION);
Uri redactedUri = shareAndGetRedactedUri(img, APP_C);
assertThat(isFileDescriptorRedacted(APP_C, redactedUri)).isTrue();
} finally {
img.delete();
uninstallAppNoThrow(APP_C);
}
}
/*
* Verify that the test app can't access unshared redacted uri via file descriptor
**/
@Test
public void testUnsharedRedactedUri_openFdForRead() throws Exception {
forceStopApp(APP_B_NO_PERMS.getPackageName());
final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
try {
// Install test app
installAppWithStoragePermissions(APP_C);
final Uri redactedUri = getRedactedUri(img);
// APP_C has R_E_S, so should have access to redactedUri
assertThat(isFileDescriptorRedacted(APP_C, redactedUri)).isTrue();
assertThrows(SecurityException.class,
() -> isFileDescriptorRedacted(APP_B_NO_PERMS, redactedUri));
} finally {
img.delete();
uninstallAppNoThrow(APP_C);
}
}
/*
* Verify that the test app can't access unshared redacted uri via file path
**/
@Test
public void testUnsharedRedactedUri_openFileForRead() throws Exception {
forceStopApp(APP_B_NO_PERMS.getPackageName());
final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
try {
// Install test app
installAppWithStoragePermissions(APP_C);
final Uri redactedUri = getRedactedUri(img);
// APP_C has R_E_S
assertThat(isFileOpenRedacted(APP_C, redactedUri)).isTrue();
assertThrows(IOException.class, () -> isFileOpenRedacted(APP_B_NO_PERMS, redactedUri));
} finally {
img.delete();
uninstallAppNoThrow(APP_C);
}
}
@Test
public void testGrantUriPermissionsForRedactedUri() throws Exception {
final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
final Uri redactedUri = getRedactedUri(img);
try {
getContext().grantUriPermission(APP_B_NO_PERMS.getPackageName(), redactedUri,
FLAG_GRANT_READ_URI_PERMISSION);
assertThrows(SecurityException.class, () ->
getContext().grantUriPermission(APP_B_NO_PERMS.getPackageName(), redactedUri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION));
} finally {
img.delete();
}
}
@Test
public void testDisallowedOperationsOnRedactedUri() throws Exception {
final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
final Uri redactedUri = getRedactedUri(img);
try {
ContentValues cv = new ContentValues();
cv.put(MediaStore.MediaColumns.DATE_ADDED, 1);
assertEquals(0, getContentResolver().update(redactedUri, new ContentValues(),
new Bundle()));
assertEquals(0, getContentResolver().delete(redactedUri, new Bundle()));
} finally {
img.delete();
}
}
@Test
public void testOpenOnRedactedUri_file() throws Exception {
final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
final Uri redactedUri = getRedactedUri(img);
try {
assertUriIsUnredacted(img);
final Cursor redactedUriCursor = getRedactedCursor(redactedUri);
File file = new File(
getStringFromCursor(redactedUriCursor, MediaStore.MediaColumns.DATA));
ExifInterface redactedExifInf = new ExifInterface(file);
assertUriIsRedacted(redactedExifInf);
assertThrows(FileNotFoundException.class, () -> new FileOutputStream(file));
} finally {
img.delete();
}
}
@Test
public void testOpenOnRedactedUri_write() throws Exception {
final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
final Uri redactedUri = getRedactedUri(img);
try {
assertThrows(UnsupportedOperationException.class,
() -> getContentResolver().openFileDescriptor(redactedUri,
"w"));
} finally {
img.delete();
}
}
@Test
public void testOpenOnRedactedUri_inputstream() throws Exception {
final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
final Uri redactedUri = getRedactedUri(img);
try {
assertUriIsUnredacted(img);
try (InputStream is = getContentResolver().openInputStream(redactedUri)) {
ExifInterface redactedExifInf = new ExifInterface(is);
assertUriIsRedacted(redactedExifInf);
}
} finally {
img.delete();
}
}
@Test
public void testOpenOnRedactedUri_read() throws Exception {
final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
final Uri redactedUri = getRedactedUri(img);
try {
assertUriIsUnredacted(img);
try (ParcelFileDescriptor pfd =
getContentResolver().openFileDescriptor(redactedUri, "r")) {
FileDescriptor fd = pfd.getFileDescriptor();
ExifInterface redactedExifInf = new ExifInterface(fd);
assertUriIsRedacted(redactedExifInf);
}
} finally {
img.delete();
}
}
private void testRedactedUriCommon(Uri uri, Uri redactedUri) {
assertEquals(redactedUri.getAuthority(), uri.getAuthority());
assertEquals(redactedUri.getScheme(), uri.getScheme());
assertNotEquals(redactedUri.getPath(), uri.getPath());
assertNotEquals(redactedUri.getPathSegments(), uri.getPathSegments());
final String uriId = redactedUri.getLastPathSegment();
assertThat(uriId.startsWith("RUID")).isTrue();
assertEquals(uriId.length(), 36);
}
private Uri shareAndGetRedactedUri(File file, TestApp testApp) {
final Uri redactedUri = getRedactedUri(file);
getContext().grantUriPermission(testApp.getPackageName(), redactedUri,
FLAG_GRANT_READ_URI_PERMISSION);
return redactedUri;
}
private Uri getRedactedUri(File file) {
final Uri uri = MediaStore.scanFile(getContentResolver(), file);
return MediaStore.getRedactedUri(getContentResolver(), uri);
}
private void assertUriIsUnredacted(File img) throws Exception {
final ExifInterface exifInterface = new ExifInterface(img);
assertNotEquals(exifInterface.getGpsDateTime(), -1);
float[] latLong = new float[]{0, 0};
exifInterface.getLatLong(latLong);
assertNotEquals(latLong[0], 0);
assertNotEquals(latLong[1], 0);
}
private void assertUriIsRedacted(ExifInterface redactedExifInf) {
assertEquals(redactedExifInf.getGpsDateTime(), -1);
float[] latLong = new float[]{0, 0};
redactedExifInf.getLatLong(latLong);
assertEquals(latLong[0], 0.0, 0.0);
assertEquals(latLong[1], 0.0, 0.0);
}
private Cursor getRedactedCursor(Uri redactedUri) {
Cursor redactedUriCursor = getContentResolver().query(redactedUri, null, null, null);
assertNotNull(redactedUriCursor);
assertThat(redactedUriCursor.moveToFirst()).isTrue();
return redactedUriCursor;
}
private String getStringFromCursor(Cursor c, String colName) {
return c.getString(c.getColumnIndex(colName));
}
private File stageImageFileWithMetadata(String name) throws Exception {
final File img = new File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), name);
try (InputStream in =
getContext().getResources().openRawResource(R.raw.img_with_metadata);
OutputStream out = new FileOutputStream(img)) {
// Dump the image we have to external storage
FileUtils.copy(in, out);
}
return img;
}
}