blob: 086a0d4326dd63f75f9373c33e344a4c43e75238 [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.app.AppOpsManager.permissionToOp;
import static android.scopedstorage.cts.lib.TestUtils.allowAppOpsToUid;
import static android.scopedstorage.cts.lib.TestUtils.createFileAs;
import static android.scopedstorage.cts.lib.TestUtils.deleteFileAs;
import static android.scopedstorage.cts.lib.TestUtils.deleteFileAsNoThrow;
import static android.scopedstorage.cts.lib.TestUtils.denyAppOpsToUid;
import static android.scopedstorage.cts.lib.TestUtils.getContentResolver;
import static android.scopedstorage.cts.lib.TestUtils.getDcimDir;
import static android.scopedstorage.cts.lib.TestUtils.getPicturesDir;
import static android.scopedstorage.cts.lib.TestUtils.installApp;
import static android.scopedstorage.cts.lib.TestUtils.installAppWithStoragePermissions;
import static android.scopedstorage.cts.lib.TestUtils.renameFileAs;
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.assertNotNull;
import static org.junit.Assert.assertNull;
import android.Manifest;
import android.app.AppOpsManager;
import android.provider.MediaStore;
import android.scopedstorage.cts.lib.TestUtils;
import android.util.Log;
import com.android.cts.install.lib.TestApp;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
import java.io.File;
/**
* Device-side test suite to verify file path operations optionally bypassing database operations.
*/
@RunWith(Parameterized.class)
public class BypassDatabaseOperationsTest extends ScopedStorageBaseDeviceTest {
static final String TAG = "BypassDatabaseOperationsTest";
// An app with READ_EXTERNAL_STORAGE permission. Targets current SDK and is preinstalled
private static final TestApp APP_SYSTEM_GALLERY_DEFAULT = new TestApp("TestAppA",
"android.scopedstorage.cts.testapp.A.withres", 1, false,
"CtsScopedStorageTestAppA.apk");
// An app with READ_EXTERNAL_STORAGE_PERMISSION. Targets current SDK and has
// requestOptimizedExternalStorageAccess=true
private static final TestApp APP_SYSTEM_GALLERY_BYPASS_DB = new TestApp(
"TestAppSystemGalleryBypassDB",
"android.scopedstorage.cts.testapp.SystemGalleryBypassDB", 1, false,
"CtsScopedStorageTestAppSystemGalleryBypassDB.apk");
// An app with READ_EXTERNAL_STORAGE_PERMISSION. Targets targetSDK=30.
private static final TestApp APP_SYSTEM_GALLERY_30 = new TestApp("TestAppC",
"android.scopedstorage.cts.testapp.C", 1, false,
"CtsScopedStorageTestAppC30.apk");
// An app with READ_EXTERNAL_STORAGE_PERMISSION. Targets targetSDK=30 and has
// requestOptimizedExternalStorageAccess=true
private static final TestApp APP_SYSTEM_GALLERY_30_BYPASS_DB = new TestApp(
"TestAppSystemGalleryBypassDB",
"android.scopedstorage.cts.testapp.SystemGalleryBypassDB", 1, false,
"CtsScopedStorageTestAppSystemGallery30BypassDB.apk");
// An app that has file manager (MANAGE_EXTERNAL_STORAGE) permission.
// Targets current SDK and preinstalled
private static final TestApp APP_FM_DEFAULT = new TestApp(
"TestAppFileManager", "android.scopedstorage.cts.testapp.filemanager", 1, false,
"CtsScopedStorageTestAppFileManager.apk");
// An app that has file manager (MANAGE_EXTERNAL_STORAGE) permission.
// Targets current SDK and has requestOptimizedExternalStorageAccess=true
private static final TestApp APP_FM_BYPASS_DATABASE_OPS = new TestApp(
"TestAppFileManagerBypassDB", "android.scopedstorage.cts.testapp.filemanagerbypassdb",
1, false, "CtsScopedStorageTestAppFileManagerBypassDB.apk");
// An app that has file manager (MANAGE_EXTERNAL_STORAGE) permission and targets targetSDK=30
private static final TestApp APP_FM_TARGETS_30 = new TestApp("TestAppC",
"android.scopedstorage.cts.testapp.C", 1, false,
"CtsScopedStorageTestAppC30.apk");
private static final String OPSTR_MANAGE_EXTERNAL_STORAGE =
permissionToOp(Manifest.permission.MANAGE_EXTERNAL_STORAGE);
private static final String[] SYSTEM_GALLERY_APPOPS = {
AppOpsManager.OPSTR_WRITE_MEDIA_IMAGES, AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO};
/**
* 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 = "BypassDatabaseOperations_file_" + NONCE + ".jpg";
@BeforeClass
public static void setupApps() throws Exception {
// File manager needs to be explicitly granted MES app op.
final int fmUid =
getContext().getPackageManager().getPackageUid(
APP_FM_DEFAULT.getPackageName(), 0);
allowAppOpsToUid(fmUid, OPSTR_MANAGE_EXTERNAL_STORAGE);
}
@Parameter(0)
public String mVolumeName;
@Parameters(name = "volume={0}")
public static Iterable<? extends Object> data() {
return ScopedStorageBaseDeviceTest.getTestParameters();
}
@Before
public void setupExternalStorage() {
super.setupExternalStorage(mVolumeName);
Log.i(TAG, "Using volume : " + mVolumeName);
}
/**
* Test that app with MANAGE_EXTERNAL_STORAGE permission and targeting
* targetSDK=31 or higher will not bypass database operations by default.
*/
@Test
public void testManageExternalStorage_DoesntBypassDatabase() throws Exception {
testAppDoesntBypassDatabaseOps(APP_FM_DEFAULT);
}
/**
* Test that app with MANAGE_EXTERNAL_STORAGE permission, targeting
* targetSDK=31 or higher and with requestOptimizedExternalStorageAccess=true
* will bypass database operations.
*/
@Test
public void testManageExternalStorage_WithBypassFlag_BypassesDatabase() throws Exception {
installApp(APP_FM_BYPASS_DATABASE_OPS);
try {
final int fmUid =
getContext().getPackageManager().getPackageUid(
APP_FM_BYPASS_DATABASE_OPS.getPackageName(), 0);
allowAppOpsToUid(fmUid, OPSTR_MANAGE_EXTERNAL_STORAGE);
testAppBypassesDatabaseOps(APP_FM_BYPASS_DATABASE_OPS);
} finally {
uninstallAppNoThrow(APP_FM_BYPASS_DATABASE_OPS);
}
}
/**
* Test that app with MANAGE_EXTERNAL_STORAGE permission and targeting
* targetSDK=30 or lower will bypass database operations by default.
*/
@Test
public void testManageExternalStorage_targets30_BypassesDatabase() throws Exception {
installApp(APP_FM_TARGETS_30);
try {
final int fmUid =
getContext().getPackageManager().getPackageUid(
APP_FM_TARGETS_30.getPackageName(), 0);
allowAppOpsToUid(fmUid, OPSTR_MANAGE_EXTERNAL_STORAGE);
testAppBypassesDatabaseOps(APP_FM_TARGETS_30);
} finally {
uninstallAppNoThrow(APP_FM_TARGETS_30);
}
}
/**
* Test that app with SYSTEM_GALLERY role and targeting
* targetSDK=current or higher will not bypass database operations by default.
*/
@Test
public void testSystemGallery_DoesntBypassDatabase() throws Exception {
final int sgUid =
getContext().getPackageManager().getPackageUid(
APP_SYSTEM_GALLERY_DEFAULT.getPackageName(), 0);
try {
allowAppOpsToUid(sgUid, SYSTEM_GALLERY_APPOPS);
testAppDoesntBypassDatabaseOps(APP_SYSTEM_GALLERY_DEFAULT);
} finally {
denyAppOpsToUid(sgUid, SYSTEM_GALLERY_APPOPS);
}
}
/**
* Test that app with SYSTEM_GALLERY role, targeting
* targetSDK=current or higher and with requestOptimizedSystemGalleryAccess=true
* will bypass database operations.
*/
@Test
public void testSystemGallery_WithBypassFlag_BypassesDatabase() throws Exception {
installAppWithStoragePermissions(APP_SYSTEM_GALLERY_BYPASS_DB);
try {
final int sgUid =
getContext().getPackageManager().getPackageUid(
APP_SYSTEM_GALLERY_BYPASS_DB.getPackageName(), 0);
allowAppOpsToUid(sgUid, SYSTEM_GALLERY_APPOPS);
testAppBypassesDatabaseOps(APP_SYSTEM_GALLERY_BYPASS_DB);
} finally {
uninstallAppNoThrow(APP_SYSTEM_GALLERY_BYPASS_DB);
}
}
/**
* Test that app with SYSTEM_GALLERY role and targeting
* targetSDK=30 or higher will not bypass database operations by default.
*/
@Test
public void testSystemGallery_targets30_DoesntBypassDatabase() throws Exception {
installAppWithStoragePermissions(APP_SYSTEM_GALLERY_30);
try {
final int sgUid =
getContext().getPackageManager().getPackageUid(
APP_SYSTEM_GALLERY_30.getPackageName(), 0);
allowAppOpsToUid(sgUid, SYSTEM_GALLERY_APPOPS);
testAppDoesntBypassDatabaseOps(APP_SYSTEM_GALLERY_30);
} finally {
uninstallAppNoThrow(APP_SYSTEM_GALLERY_30);
}
}
/**
* Test that app with SYSTEM_GALLERY role, targeting
* targetSDK=30 or higher and with requestOptimizedSystemGalleryAccess=true
* will bypass database operations.
*/
@Test
public void testSystemGallery_targets30_WithBypassFlag_BypassesDatabase() throws Exception {
installAppWithStoragePermissions(APP_SYSTEM_GALLERY_30_BYPASS_DB);
try {
final int sgUid =
getContext().getPackageManager().getPackageUid(
APP_SYSTEM_GALLERY_30_BYPASS_DB.getPackageName(), 0);
allowAppOpsToUid(sgUid, SYSTEM_GALLERY_APPOPS);
testAppBypassesDatabaseOps(APP_SYSTEM_GALLERY_30_BYPASS_DB);
} finally {
uninstallAppNoThrow(APP_SYSTEM_GALLERY_30_BYPASS_DB);
}
}
private void testAppDoesntBypassDatabaseOps(TestApp app) throws Exception {
final File file = new File(getDcimDir(), IMAGE_FILE_NAME);
final File renamedFile = new File(getPicturesDir(), IMAGE_FILE_NAME);
try {
assertThat(createFileAs(app, file.getAbsolutePath())).isTrue();
// File path create() added file to database.
assertThat(TestUtils.checkDatabaseRowExistsAs(app, file)).isTrue();
assertThat(renameFileAs(app, file, renamedFile)).isTrue();
// File path rename() also updates the database row
assertThat(TestUtils.checkDatabaseRowExistsAs(app, file)).isFalse();
assertThat(TestUtils.checkDatabaseRowExistsAs(app, renamedFile)).isTrue();
assertThat(deleteFileAs(app, renamedFile.getAbsolutePath())).isTrue();
// File path delete() removes database row.
assertThat(TestUtils.checkDatabaseRowExistsAs(app, renamedFile)).isFalse();
} finally {
if (file.exists()) {
deleteFileAsNoThrow(app, file.getAbsolutePath());
}
if (renamedFile.exists()) {
deleteFileAsNoThrow(app, renamedFile.getAbsolutePath());
}
}
}
private void testAppBypassesDatabaseOps(TestApp app) throws Exception {
final File file = new File(getDcimDir(), IMAGE_FILE_NAME);
final File renamedFile = new File(getPicturesDir(), IMAGE_FILE_NAME);
try {
assertThat(createFileAs(app, file.getAbsolutePath())).isTrue();
// File path create() didn't add the file to database.
assertThat(TestUtils.checkDatabaseRowExistsAs(app, file)).isFalse();
// Ensure file is added to database.
assertNotNull(MediaStore.scanFile(getContentResolver(), file));
assertThat(renameFileAs(app, file, renamedFile)).isTrue();
// Rename() didn't update the database row.
assertThat(TestUtils.checkDatabaseRowExistsAs(app, file)).isTrue();
assertThat(TestUtils.checkDatabaseRowExistsAs(app, renamedFile)).isFalse();
// Ensure database is updated with renamed path
assertNull(MediaStore.scanFile(getContentResolver(), file));
assertNotNull(MediaStore.scanFile(getContentResolver(), renamedFile));
assertThat(TestUtils.checkDatabaseRowExistsAs(app, renamedFile)).isTrue();
assertThat(deleteFileAs(app, renamedFile.getAbsolutePath())).isTrue();
// Unlink() didn't remove the database row.
assertThat(TestUtils.checkDatabaseRowExistsAs(app, renamedFile)).isTrue();
} finally {
if (file.exists()) {
deleteFileAsNoThrow(app, file.getAbsolutePath());
}
if (renamedFile.exists()) {
deleteFileAsNoThrow(app, renamedFile.getAbsolutePath());
}
MediaStore.scanFile(getContentResolver(), file);
MediaStore.scanFile(getContentResolver(), renamedFile);
}
}
}