| /* |
| * 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); |
| } |
| } |
| } |