blob: 6eb738e2230ee90629ce9ce8f4a58b245d8498ca [file] [log] [blame]
/*
* Copyright (C) 2020 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.providers.media;
import static android.Manifest.permission.ACCESS_MEDIA_LOCATION;
import static android.Manifest.permission.MANAGE_APP_OPS_MODES;
import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE;
import static android.Manifest.permission.MANAGE_MEDIA;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.Manifest.permission.UPDATE_APP_OPS_STATS;
import static androidx.test.InstrumentationRegistry.getContext;
import static com.android.providers.media.PermissionActivity.VERB_FAVORITE;
import static com.android.providers.media.PermissionActivity.VERB_TRASH;
import static com.android.providers.media.PermissionActivity.VERB_UNFAVORITE;
import static com.android.providers.media.PermissionActivity.VERB_WRITE;
import static com.android.providers.media.PermissionActivity.shouldShowActionDialog;
import static com.android.providers.media.util.PermissionUtils.checkPermissionAccessMediaLocation;
import static com.android.providers.media.util.PermissionUtils.checkPermissionManageMedia;
import static com.android.providers.media.util.PermissionUtils.checkPermissionManager;
import static com.android.providers.media.util.PermissionUtils.checkPermissionReadStorage;
import static com.android.providers.media.util.TestUtils.adoptShellPermission;
import static com.android.providers.media.util.TestUtils.dropShellPermission;
import static com.google.common.truth.Truth.assertThat;
import android.app.AppOpsManager;
import android.app.Instrumentation;
import android.content.ClipData;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Environment;
import android.provider.MediaStore;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SdkSuppress;
import androidx.test.runner.AndroidJUnit4;
import com.android.providers.media.scan.MediaScannerTest;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
import java.util.HashSet;
import java.util.concurrent.TimeoutException;
/**
* We already have solid coverage of this logic in {@code CtsProviderTestCases},
* but the coverage system currently doesn't measure that, so we add the bare
* minimum local testing here to convince the tooling that it's covered.
*/
@RunWith(AndroidJUnit4.class)
public class PermissionActivityTest {
private static final String TEST_APP_PACKAGE_NAME =
"com.android.providers.media.testapp.permission";
private static final String OP_ACCESS_MEDIA_LOCATION =
AppOpsManager.permissionToOp(ACCESS_MEDIA_LOCATION);
private static final String OP_MANAGE_MEDIA =
AppOpsManager.permissionToOp(MANAGE_MEDIA);
private static final String OP_MANAGE_EXTERNAL_STORAGE =
AppOpsManager.permissionToOp(MANAGE_EXTERNAL_STORAGE);
private static final String OP_READ_EXTERNAL_STORAGE =
AppOpsManager.permissionToOp(READ_EXTERNAL_STORAGE);
// The list is used to restore the permissions after the test is finished.
// The default value for these app ops is {@link AppOpsManager#MODE_DEFAULT}
private static final String[] DEFAULT_OP_PERMISSION_LIST = new String[] {
OP_MANAGE_EXTERNAL_STORAGE,
OP_MANAGE_MEDIA
};
// The list is used to restore the permissions after the test is finished.
// The default value for these app ops is {@link AppOpsManager#MODE_ALLOWED}
private static final String[] ALLOWED_OP_PERMISSION_LIST = new String[] {
OP_ACCESS_MEDIA_LOCATION,
OP_READ_EXTERNAL_STORAGE
};
private static final long TIMEOUT_MILLIS = 3000;
private static final long SLEEP_MILLIS = 30;
private static final int TEST_APP_PID = -1;
private int mTestAppUid = -1;
@Before
public void setUp() throws Exception {
mTestAppUid = getContext().getPackageManager().getPackageUid(TEST_APP_PACKAGE_NAME, 0);
}
@Test
public void testSimple() throws Exception {
final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
final Intent intent = new Intent(inst.getContext(), GetResultActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
final GetResultActivity activity = (GetResultActivity) inst.startActivitySync(intent);
activity.startActivityForResult(createIntent(), 42);
}
@Test
public void testShouldShowActionDialog_favorite_false() throws Exception {
assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
TEST_APP_PACKAGE_NAME, null, VERB_FAVORITE)).isFalse();
}
@Test
public void testShouldShowActionDialog_unfavorite_false() throws Exception {
assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
TEST_APP_PACKAGE_NAME, null, VERB_UNFAVORITE)).isFalse();
}
@Test
@SdkSuppress(minSdkVersion = 31, codeName = "S")
public void testShouldShowActionDialog_noRESAndMES_true() throws Exception {
final String[] enableAppOpsList = {OP_MANAGE_MEDIA};
final String[] disableAppOpsList = {OP_MANAGE_EXTERNAL_STORAGE, OP_READ_EXTERNAL_STORAGE};
adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
try {
setupPermissions(mTestAppUid, enableAppOpsList, disableAppOpsList);
assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
TEST_APP_PACKAGE_NAME, null, VERB_TRASH)).isTrue();
} finally {
restoreDefaultAppOpPermissions(mTestAppUid);
dropShellPermission();
}
}
@Test
@SdkSuppress(minSdkVersion = 31, codeName = "S")
public void testShouldShowActionDialog_noMANAGE_MEDIA_true() throws Exception {
final String[] enableAppOpsList = {OP_MANAGE_EXTERNAL_STORAGE, OP_READ_EXTERNAL_STORAGE};
final String[] disableAppOpsList = {OP_MANAGE_MEDIA};
adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
try {
setupPermissions(mTestAppUid, enableAppOpsList, disableAppOpsList);
assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
TEST_APP_PACKAGE_NAME, null, VERB_TRASH)).isTrue();
} finally {
restoreDefaultAppOpPermissions(mTestAppUid);
dropShellPermission();
}
}
@Test
@SdkSuppress(minSdkVersion = 31, codeName = "S")
public void testShouldShowActionDialog_hasPermissionWithRES_false() throws Exception {
final String[] enableAppOpsList = {OP_MANAGE_MEDIA, OP_READ_EXTERNAL_STORAGE};
final String[] disableAppOpsList = {OP_MANAGE_EXTERNAL_STORAGE};
adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
try {
setupPermissions(mTestAppUid, enableAppOpsList, disableAppOpsList);
assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
TEST_APP_PACKAGE_NAME, null, VERB_TRASH)).isFalse();
} finally {
restoreDefaultAppOpPermissions(mTestAppUid);
dropShellPermission();
}
}
@Test
@SdkSuppress(minSdkVersion = 31, codeName = "S")
public void testShouldShowActionDialog_hasPermissionWithMES_false() throws Exception {
final String[] enableAppOpsList = {OP_MANAGE_EXTERNAL_STORAGE, OP_MANAGE_MEDIA};
final String[] disableAppOpsList = {OP_READ_EXTERNAL_STORAGE};
adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
try {
setupPermissions(mTestAppUid, enableAppOpsList, disableAppOpsList);
assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
TEST_APP_PACKAGE_NAME, null, VERB_TRASH)).isFalse();
} finally {
restoreDefaultAppOpPermissions(mTestAppUid);
dropShellPermission();
}
}
@Test
@SdkSuppress(minSdkVersion = 31, codeName = "S")
public void testShouldShowActionDialog_writeNoACCESS_MEDIA_LOCATION_true() throws Exception {
final String[] enableAppOpsList =
{OP_MANAGE_EXTERNAL_STORAGE, OP_MANAGE_MEDIA, OP_READ_EXTERNAL_STORAGE};
final String[] disableAppOpsList = {OP_ACCESS_MEDIA_LOCATION};
adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
try {
setupPermissions(mTestAppUid, enableAppOpsList, disableAppOpsList);
assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
TEST_APP_PACKAGE_NAME, null, VERB_WRITE)).isTrue();
} finally {
restoreDefaultAppOpPermissions(mTestAppUid);
dropShellPermission();
}
}
@Test
@SdkSuppress(minSdkVersion = 31, codeName = "S")
public void testShouldShowActionDialog_writeHasACCESS_MEDIA_LOCATION_false() throws Exception {
final String[] enableAppOpsList = {
OP_ACCESS_MEDIA_LOCATION,
OP_MANAGE_EXTERNAL_STORAGE,
OP_MANAGE_MEDIA,
OP_READ_EXTERNAL_STORAGE};
final String[] disableAppOpsList = new String[]{};
adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
try {
setupPermissions(mTestAppUid, enableAppOpsList, disableAppOpsList);
assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
TEST_APP_PACKAGE_NAME, null, VERB_WRITE)).isFalse();
} finally {
restoreDefaultAppOpPermissions(mTestAppUid);
dropShellPermission();
}
}
private static void setupPermissions(int uid, @NonNull String[] enableAppOpsList,
@NonNull String[] disableAppOpsList) throws Exception {
for (String op : enableAppOpsList) {
modifyAppOp(uid, op, AppOpsManager.MODE_ALLOWED);
}
for (String op : disableAppOpsList) {
modifyAppOp(uid, op, AppOpsManager.MODE_ERRORED);
}
pollForAppOpPermissions(TEST_APP_PID, uid, enableAppOpsList, /* hasPermission= */ true);
pollForAppOpPermissions(TEST_APP_PID, uid, disableAppOpsList, /* hasPermission= */ false);
}
private static void restoreDefaultAppOpPermissions(int uid) {
for (String op : DEFAULT_OP_PERMISSION_LIST) {
modifyAppOp(uid, op, AppOpsManager.MODE_DEFAULT);
}
for (String op : ALLOWED_OP_PERMISSION_LIST) {
modifyAppOp(uid, op, AppOpsManager.MODE_ALLOWED);
}
}
private static Intent createIntent() throws Exception {
final Context context = InstrumentationRegistry.getContext();
final File dir = Environment
.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
final File file = MediaScannerTest.stage(R.raw.test_image,
new File(dir, "test" + System.nanoTime() + ".jpg"));
final Uri uri = MediaStore.scanFile(context.getContentResolver(), file);
final Intent intent = new Intent(MediaStore.CREATE_WRITE_REQUEST_CALL, null,
context, PermissionActivity.class);
intent.putExtra(MediaStore.EXTRA_CLIP_DATA, ClipData.newRawUri("", uri));
intent.putExtra(MediaStore.EXTRA_CONTENT_VALUES, new ContentValues());
return intent;
}
private static void modifyAppOp(int uid, @NonNull String op, int mode) {
getContext().getSystemService(AppOpsManager.class).setUidMode(op, uid, mode);
}
private static void pollForAppOpPermissions(int pid, int uid, String[] opList,
boolean hasPermission) throws Exception {
long current = System.currentTimeMillis();
final long timeout = current + TIMEOUT_MILLIS;
final HashSet<String> checkedOpSet = new HashSet<>();
while (current < timeout && checkedOpSet.size() < opList.length) {
for (String op : opList) {
if (!checkedOpSet.contains(op) && checkPermission(op, pid, uid,
TEST_APP_PACKAGE_NAME, hasPermission)) {
checkedOpSet.add(op);
continue;
}
}
Thread.sleep(SLEEP_MILLIS);
current = System.currentTimeMillis();
}
if (checkedOpSet.size() != opList.length) {
throw new TimeoutException("Check AppOp permissions with " + uid + " timeout");
}
}
private static boolean checkPermission(@NonNull String op, int pid, int uid,
@NonNull String packageName, boolean expected) {
final Context context = getContext();
if (TextUtils.equals(op, OP_READ_EXTERNAL_STORAGE)) {
return expected == checkPermissionReadStorage(context, pid, uid, packageName,
/* attributionTag= */ null);
} else if (TextUtils.equals(op, OP_MANAGE_EXTERNAL_STORAGE)) {
return expected == checkPermissionManager(context, pid, uid, packageName,
/* attributionTag= */ null);
} else if (TextUtils.equals(op, OP_MANAGE_MEDIA)) {
return expected == checkPermissionManageMedia(context, pid, uid, packageName,
/* attributionTag= */ null);
} else if (TextUtils.equals(op, OP_ACCESS_MEDIA_LOCATION)) {
return expected == checkPermissionAccessMediaLocation(context, pid, uid,
packageName, /* attributionTag= */ null);
} else {
throw new IllegalArgumentException("checkPermission is not supported for op: " + op);
}
}
}