1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.providers.media; 18 19 import static android.Manifest.permission.ACCESS_MEDIA_LOCATION; 20 import static android.Manifest.permission.MANAGE_APP_OPS_MODES; 21 import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE; 22 import static android.Manifest.permission.MANAGE_MEDIA; 23 import static android.Manifest.permission.READ_EXTERNAL_STORAGE; 24 import static android.Manifest.permission.UPDATE_APP_OPS_STATS; 25 26 import static androidx.test.InstrumentationRegistry.getContext; 27 28 import static com.android.providers.media.PermissionActivity.VERB_FAVORITE; 29 import static com.android.providers.media.PermissionActivity.VERB_TRASH; 30 import static com.android.providers.media.PermissionActivity.VERB_UNFAVORITE; 31 import static com.android.providers.media.PermissionActivity.VERB_WRITE; 32 import static com.android.providers.media.PermissionActivity.shouldShowActionDialog; 33 import static com.android.providers.media.util.PermissionUtils.checkPermissionAccessMediaLocation; 34 import static com.android.providers.media.util.PermissionUtils.checkPermissionManageMedia; 35 import static com.android.providers.media.util.PermissionUtils.checkPermissionManager; 36 import static com.android.providers.media.util.PermissionUtils.checkPermissionReadStorage; 37 import static com.android.providers.media.util.TestUtils.adoptShellPermission; 38 import static com.android.providers.media.util.TestUtils.dropShellPermission; 39 40 import static com.google.common.truth.Truth.assertThat; 41 42 import android.app.AppOpsManager; 43 import android.app.Instrumentation; 44 import android.content.ClipData; 45 import android.content.ContentValues; 46 import android.content.Context; 47 import android.content.Intent; 48 import android.net.Uri; 49 import android.os.Environment; 50 import android.provider.MediaStore; 51 import android.text.TextUtils; 52 53 import androidx.annotation.NonNull; 54 import androidx.test.InstrumentationRegistry; 55 import androidx.test.filters.SdkSuppress; 56 import androidx.test.runner.AndroidJUnit4; 57 58 import com.android.providers.media.scan.MediaScannerTest; 59 60 import org.junit.Before; 61 import org.junit.Test; 62 import org.junit.runner.RunWith; 63 64 import java.io.File; 65 import java.util.HashSet; 66 import java.util.concurrent.TimeoutException; 67 68 /** 69 * We already have solid coverage of this logic in {@code CtsProviderTestCases}, 70 * but the coverage system currently doesn't measure that, so we add the bare 71 * minimum local testing here to convince the tooling that it's covered. 72 */ 73 @RunWith(AndroidJUnit4.class) 74 public class PermissionActivityTest { 75 private static final String TEST_APP_PACKAGE_NAME = 76 "com.android.providers.media.testapp.permission"; 77 78 private static final String OP_ACCESS_MEDIA_LOCATION = 79 AppOpsManager.permissionToOp(ACCESS_MEDIA_LOCATION); 80 private static final String OP_MANAGE_MEDIA = 81 AppOpsManager.permissionToOp(MANAGE_MEDIA); 82 private static final String OP_MANAGE_EXTERNAL_STORAGE = 83 AppOpsManager.permissionToOp(MANAGE_EXTERNAL_STORAGE); 84 private static final String OP_READ_EXTERNAL_STORAGE = 85 AppOpsManager.permissionToOp(READ_EXTERNAL_STORAGE); 86 87 // The list is used to restore the permissions after the test is finished. 88 // The default value for these app ops is {@link AppOpsManager#MODE_DEFAULT} 89 private static final String[] DEFAULT_OP_PERMISSION_LIST = new String[] { 90 OP_MANAGE_EXTERNAL_STORAGE, 91 OP_MANAGE_MEDIA 92 }; 93 94 // The list is used to restore the permissions after the test is finished. 95 // The default value for these app ops is {@link AppOpsManager#MODE_ALLOWED} 96 private static final String[] ALLOWED_OP_PERMISSION_LIST = new String[] { 97 OP_ACCESS_MEDIA_LOCATION, 98 OP_READ_EXTERNAL_STORAGE 99 }; 100 101 private static final long TIMEOUT_MILLIS = 3000; 102 private static final long SLEEP_MILLIS = 30; 103 104 private static final int TEST_APP_PID = -1; 105 private int mTestAppUid = -1; 106 107 @Before setUp()108 public void setUp() throws Exception { 109 mTestAppUid = getContext().getPackageManager().getPackageUid(TEST_APP_PACKAGE_NAME, 0); 110 } 111 112 @Test testSimple()113 public void testSimple() throws Exception { 114 final Instrumentation inst = InstrumentationRegistry.getInstrumentation(); 115 final Intent intent = new Intent(inst.getContext(), GetResultActivity.class); 116 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 117 118 final GetResultActivity activity = (GetResultActivity) inst.startActivitySync(intent); 119 activity.startActivityForResult(createIntent(), 42); 120 } 121 122 @Test testShouldShowActionDialog_favorite_false()123 public void testShouldShowActionDialog_favorite_false() throws Exception { 124 assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid, 125 TEST_APP_PACKAGE_NAME, null, VERB_FAVORITE)).isFalse(); 126 } 127 128 @Test testShouldShowActionDialog_unfavorite_false()129 public void testShouldShowActionDialog_unfavorite_false() throws Exception { 130 assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid, 131 TEST_APP_PACKAGE_NAME, null, VERB_UNFAVORITE)).isFalse(); 132 } 133 134 @Test 135 @SdkSuppress(minSdkVersion = 31, codeName = "S") testShouldShowActionDialog_noRESAndMES_true()136 public void testShouldShowActionDialog_noRESAndMES_true() throws Exception { 137 final String[] enableAppOpsList = {OP_MANAGE_MEDIA}; 138 final String[] disableAppOpsList = {OP_MANAGE_EXTERNAL_STORAGE, OP_READ_EXTERNAL_STORAGE}; 139 adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES); 140 141 try { 142 setupPermissions(mTestAppUid, enableAppOpsList, disableAppOpsList); 143 144 assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid, 145 TEST_APP_PACKAGE_NAME, null, VERB_TRASH)).isTrue(); 146 } finally { 147 restoreDefaultAppOpPermissions(mTestAppUid); 148 dropShellPermission(); 149 } 150 } 151 152 @Test 153 @SdkSuppress(minSdkVersion = 31, codeName = "S") testShouldShowActionDialog_noMANAGE_MEDIA_true()154 public void testShouldShowActionDialog_noMANAGE_MEDIA_true() throws Exception { 155 final String[] enableAppOpsList = {OP_MANAGE_EXTERNAL_STORAGE, OP_READ_EXTERNAL_STORAGE}; 156 final String[] disableAppOpsList = {OP_MANAGE_MEDIA}; 157 adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES); 158 159 try { 160 setupPermissions(mTestAppUid, enableAppOpsList, disableAppOpsList); 161 162 assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid, 163 TEST_APP_PACKAGE_NAME, null, VERB_TRASH)).isTrue(); 164 } finally { 165 restoreDefaultAppOpPermissions(mTestAppUid); 166 dropShellPermission(); 167 } 168 } 169 170 @Test 171 @SdkSuppress(minSdkVersion = 31, codeName = "S") testShouldShowActionDialog_hasPermissionWithRES_false()172 public void testShouldShowActionDialog_hasPermissionWithRES_false() throws Exception { 173 final String[] enableAppOpsList = {OP_MANAGE_MEDIA, OP_READ_EXTERNAL_STORAGE}; 174 final String[] disableAppOpsList = {OP_MANAGE_EXTERNAL_STORAGE}; 175 adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES); 176 177 try { 178 setupPermissions(mTestAppUid, enableAppOpsList, disableAppOpsList); 179 180 assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid, 181 TEST_APP_PACKAGE_NAME, null, VERB_TRASH)).isFalse(); 182 } finally { 183 restoreDefaultAppOpPermissions(mTestAppUid); 184 dropShellPermission(); 185 } 186 } 187 188 @Test 189 @SdkSuppress(minSdkVersion = 31, codeName = "S") testShouldShowActionDialog_hasPermissionWithMES_false()190 public void testShouldShowActionDialog_hasPermissionWithMES_false() throws Exception { 191 final String[] enableAppOpsList = {OP_MANAGE_EXTERNAL_STORAGE, OP_MANAGE_MEDIA}; 192 final String[] disableAppOpsList = {OP_READ_EXTERNAL_STORAGE}; 193 adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES); 194 195 try { 196 setupPermissions(mTestAppUid, enableAppOpsList, disableAppOpsList); 197 198 assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid, 199 TEST_APP_PACKAGE_NAME, null, VERB_TRASH)).isFalse(); 200 } finally { 201 restoreDefaultAppOpPermissions(mTestAppUid); 202 dropShellPermission(); 203 } 204 } 205 206 @Test 207 @SdkSuppress(minSdkVersion = 31, codeName = "S") testShouldShowActionDialog_writeNoACCESS_MEDIA_LOCATION_true()208 public void testShouldShowActionDialog_writeNoACCESS_MEDIA_LOCATION_true() throws Exception { 209 final String[] enableAppOpsList = 210 {OP_MANAGE_EXTERNAL_STORAGE, OP_MANAGE_MEDIA, OP_READ_EXTERNAL_STORAGE}; 211 final String[] disableAppOpsList = {OP_ACCESS_MEDIA_LOCATION}; 212 adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES); 213 214 try { 215 setupPermissions(mTestAppUid, enableAppOpsList, disableAppOpsList); 216 217 assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid, 218 TEST_APP_PACKAGE_NAME, null, VERB_WRITE)).isTrue(); 219 } finally { 220 restoreDefaultAppOpPermissions(mTestAppUid); 221 dropShellPermission(); 222 } 223 } 224 225 @Test 226 @SdkSuppress(minSdkVersion = 31, codeName = "S") testShouldShowActionDialog_writeHasACCESS_MEDIA_LOCATION_false()227 public void testShouldShowActionDialog_writeHasACCESS_MEDIA_LOCATION_false() throws Exception { 228 final String[] enableAppOpsList = { 229 OP_ACCESS_MEDIA_LOCATION, 230 OP_MANAGE_EXTERNAL_STORAGE, 231 OP_MANAGE_MEDIA, 232 OP_READ_EXTERNAL_STORAGE}; 233 final String[] disableAppOpsList = new String[]{}; 234 adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES); 235 236 try { 237 setupPermissions(mTestAppUid, enableAppOpsList, disableAppOpsList); 238 239 assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid, 240 TEST_APP_PACKAGE_NAME, null, VERB_WRITE)).isFalse(); 241 } finally { 242 restoreDefaultAppOpPermissions(mTestAppUid); 243 dropShellPermission(); 244 } 245 } 246 setupPermissions(int uid, @NonNull String[] enableAppOpsList, @NonNull String[] disableAppOpsList)247 private static void setupPermissions(int uid, @NonNull String[] enableAppOpsList, 248 @NonNull String[] disableAppOpsList) throws Exception { 249 for (String op : enableAppOpsList) { 250 modifyAppOp(uid, op, AppOpsManager.MODE_ALLOWED); 251 } 252 253 for (String op : disableAppOpsList) { 254 modifyAppOp(uid, op, AppOpsManager.MODE_ERRORED); 255 } 256 257 pollForAppOpPermissions(TEST_APP_PID, uid, enableAppOpsList, /* hasPermission= */ true); 258 pollForAppOpPermissions(TEST_APP_PID, uid, disableAppOpsList, /* hasPermission= */ false); 259 } 260 restoreDefaultAppOpPermissions(int uid)261 private static void restoreDefaultAppOpPermissions(int uid) { 262 for (String op : DEFAULT_OP_PERMISSION_LIST) { 263 modifyAppOp(uid, op, AppOpsManager.MODE_DEFAULT); 264 } 265 266 for (String op : ALLOWED_OP_PERMISSION_LIST) { 267 modifyAppOp(uid, op, AppOpsManager.MODE_ALLOWED); 268 } 269 } 270 createIntent()271 private static Intent createIntent() throws Exception { 272 final Context context = InstrumentationRegistry.getContext(); 273 274 final File dir = Environment 275 .getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); 276 final File file = MediaScannerTest.stage(R.raw.test_image, 277 new File(dir, "test" + System.nanoTime() + ".jpg")); 278 final Uri uri = MediaStore.scanFile(context.getContentResolver(), file); 279 280 final Intent intent = new Intent(MediaStore.CREATE_WRITE_REQUEST_CALL, null, 281 context, PermissionActivity.class); 282 intent.putExtra(MediaStore.EXTRA_CLIP_DATA, ClipData.newRawUri("", uri)); 283 intent.putExtra(MediaStore.EXTRA_CONTENT_VALUES, new ContentValues()); 284 return intent; 285 } 286 modifyAppOp(int uid, @NonNull String op, int mode)287 private static void modifyAppOp(int uid, @NonNull String op, int mode) { 288 getContext().getSystemService(AppOpsManager.class).setUidMode(op, uid, mode); 289 } 290 pollForAppOpPermissions(int pid, int uid, String[] opList, boolean hasPermission)291 private static void pollForAppOpPermissions(int pid, int uid, String[] opList, 292 boolean hasPermission) throws Exception { 293 long current = System.currentTimeMillis(); 294 final long timeout = current + TIMEOUT_MILLIS; 295 final HashSet<String> checkedOpSet = new HashSet<>(); 296 297 while (current < timeout && checkedOpSet.size() < opList.length) { 298 for (String op : opList) { 299 if (!checkedOpSet.contains(op) && checkPermission(op, pid, uid, 300 TEST_APP_PACKAGE_NAME, hasPermission)) { 301 checkedOpSet.add(op); 302 continue; 303 } 304 } 305 Thread.sleep(SLEEP_MILLIS); 306 current = System.currentTimeMillis(); 307 } 308 309 if (checkedOpSet.size() != opList.length) { 310 throw new TimeoutException("Check AppOp permissions with " + uid + " timeout"); 311 } 312 } 313 checkPermission(@onNull String op, int pid, int uid, @NonNull String packageName, boolean expected)314 private static boolean checkPermission(@NonNull String op, int pid, int uid, 315 @NonNull String packageName, boolean expected) { 316 final Context context = getContext(); 317 318 if (TextUtils.equals(op, OP_READ_EXTERNAL_STORAGE)) { 319 return expected == checkPermissionReadStorage(context, pid, uid, packageName, 320 /* attributionTag= */ null); 321 } else if (TextUtils.equals(op, OP_MANAGE_EXTERNAL_STORAGE)) { 322 return expected == checkPermissionManager(context, pid, uid, packageName, 323 /* attributionTag= */ null); 324 } else if (TextUtils.equals(op, OP_MANAGE_MEDIA)) { 325 return expected == checkPermissionManageMedia(context, pid, uid, packageName, 326 /* attributionTag= */ null); 327 } else if (TextUtils.equals(op, OP_ACCESS_MEDIA_LOCATION)) { 328 return expected == checkPermissionAccessMediaLocation(context, pid, uid, 329 packageName, /* attributionTag= */ null); 330 } else { 331 throw new IllegalArgumentException("checkPermission is not supported for op: " + op); 332 } 333 } 334 } 335