1 /* 2 * Copyright (C) 2023 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.MANAGE_APP_OPS_MODES; 20 import static android.Manifest.permission.UPDATE_APP_OPS_STATS; 21 import static android.app.AppOpsManager.OPSTR_READ_MEDIA_IMAGES; 22 import static android.app.AppOpsManager.OPSTR_READ_MEDIA_VIDEO; 23 import static android.app.AppOpsManager.OPSTR_READ_MEDIA_VISUAL_USER_SELECTED; 24 import static android.provider.MediaStore.grantMediaReadForPackage; 25 26 import static com.android.providers.media.util.FileCreationUtils.insertFileInResolver; 27 import static com.android.providers.media.util.TestUtils.dropShellPermission; 28 29 import static com.google.common.truth.Truth.assertThat; 30 import static com.google.common.truth.Truth.assertWithMessage; 31 32 import android.Manifest; 33 import android.app.AppOpsManager; 34 import android.content.Context; 35 import android.content.pm.PackageManager; 36 import android.database.Cursor; 37 import android.net.Uri; 38 import android.os.Bundle; 39 40 import androidx.test.InstrumentationRegistry; 41 import androidx.test.filters.SdkSuppress; 42 import androidx.test.runner.AndroidJUnit4; 43 44 import com.android.cts.install.lib.TestApp; 45 import com.android.providers.media.util.FileCreationUtils; 46 47 import org.junit.AfterClass; 48 import org.junit.BeforeClass; 49 import org.junit.Test; 50 import org.junit.runner.RunWith; 51 52 import java.util.List; 53 54 @SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake") 55 @RunWith(AndroidJUnit4.class) 56 public class MediaGrantsAppOpStateTest { 57 private static final TestApp TEST_APP_WITH_USER_SELECTED_PERMS = 58 new TestApp( 59 "TestAppWithUserSelectedPerms", 60 "com.android.providers.media.testapp.withuserselectedperms", 61 1, 62 false, 63 "MediaProviderTestAppWithUserSelectedPerms.apk"); 64 private static final String TEST_APP_PACKAGE_NAME = 65 TEST_APP_WITH_USER_SELECTED_PERMS.getPackageName(); 66 67 private static Context sIsolatedContext; 68 private static DatabaseHelper sExternalDatabase; 69 private static int sTestAppUid; 70 private static List<Uri> sUriList; 71 private static AppOpsManager sAppOpsManager; 72 private static final Object sLock = new Object(); 73 private static AppOpsManager.OnOpChangedListener sOnOpChangedListener = 74 (op, packageName) -> sLock.notify(); 75 76 @BeforeClass setUp()77 public static void setUp() throws Exception { 78 androidx.test.platform.app.InstrumentationRegistry.getInstrumentation() 79 .getUiAutomation() 80 .adoptShellPermissionIdentity( 81 android.Manifest.permission.LOG_COMPAT_CHANGE, 82 android.Manifest.permission.READ_COMPAT_CHANGE_CONFIG, 83 android.Manifest.permission.READ_DEVICE_CONFIG, 84 android.Manifest.permission.INTERACT_ACROSS_USERS, 85 android.Manifest.permission.WRITE_MEDIA_STORAGE, 86 Manifest.permission.MANAGE_EXTERNAL_STORAGE, 87 // only needed for this test 88 UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES); 89 Context context = InstrumentationRegistry.getTargetContext(); 90 sIsolatedContext = new IsolatedContext(context, "modern", /*asFuseThread*/ false); 91 sExternalDatabase = ((IsolatedContext) sIsolatedContext).getExternalDatabase(); 92 sAppOpsManager = context.getSystemService(AppOpsManager.class); 93 Long fileId1 = insertFileInResolver(sIsolatedContext.getContentResolver(), "test_file1"); 94 Long fileId2 = insertFileInResolver(sIsolatedContext.getContentResolver(), "test_file2"); 95 sUriList = List.of(FileCreationUtils.buildValidPickerUri(fileId1), 96 FileCreationUtils.buildValidPickerUri(fileId2)); 97 sTestAppUid = context.getPackageManager() 98 .getPackageUid(TEST_APP_PACKAGE_NAME, PackageManager.PackageInfoFlags.of(0)); 99 } 100 101 @AfterClass tearDown()102 public static void tearDown() { 103 try { 104 for (Uri uri: sUriList) { 105 sIsolatedContext.getContentResolver().delete(uri, Bundle.EMPTY); 106 } 107 } catch (Exception ignored) { 108 } 109 dropShellPermission(); 110 } 111 112 @Test testAppOpStateChangeToAllowAll()113 public void testAppOpStateChangeToAllowAll() throws Exception { 114 // Set the initial state to User Select mode 115 denyAppOp(OPSTR_READ_MEDIA_IMAGES); 116 denyAppOp(OPSTR_READ_MEDIA_VIDEO); 117 allowAppOp(OPSTR_READ_MEDIA_VISUAL_USER_SELECTED); 118 119 grantMediaReadForPackage(sIsolatedContext, sTestAppUid, sUriList); 120 // verify we can see the grant 121 assertThat(getRowCountForTestPackage()).isEqualTo(sUriList.size()); 122 123 // Change the state to Allow All 124 allowAppOp(OPSTR_READ_MEDIA_IMAGES); 125 allowAppOp(OPSTR_READ_MEDIA_VIDEO); 126 // Verify that grants are removed 127 assertThat(getRowCountForTestPackage()).isEqualTo(0); 128 129 // Change the state back to "Select flow" 130 denyAppOp(OPSTR_READ_MEDIA_IMAGES); 131 denyAppOp(OPSTR_READ_MEDIA_VIDEO); 132 assertThat(getRowCountForTestPackage()).isEqualTo(0); 133 } 134 135 @Test testAppOpStateChangeToDenyAll()136 public void testAppOpStateChangeToDenyAll() throws Exception { 137 // Set the initial state to User Select mode 138 denyAppOp(OPSTR_READ_MEDIA_IMAGES); 139 denyAppOp(OPSTR_READ_MEDIA_VIDEO); 140 allowAppOp(OPSTR_READ_MEDIA_VISUAL_USER_SELECTED); 141 142 grantMediaReadForPackage(sIsolatedContext, sTestAppUid, sUriList); 143 // verify we can see the grant 144 assertThat(getRowCountForTestPackage()).isEqualTo(sUriList.size()); 145 146 // Change the state to deny all 147 denyAppOp(OPSTR_READ_MEDIA_VISUAL_USER_SELECTED); 148 assertThat(getRowCountForTestPackage()).isEqualTo(0); 149 150 // Change the state back to "Select Flow" 151 allowAppOp(OPSTR_READ_MEDIA_VISUAL_USER_SELECTED); 152 assertThat(getRowCountForTestPackage()).isEqualTo(0); 153 } 154 155 @Test testGrantSelectFlowDoesntClearGrants()156 public void testGrantSelectFlowDoesntClearGrants() throws Exception { 157 // Set the initial state to deny all 158 denyAppOp(OPSTR_READ_MEDIA_IMAGES); 159 denyAppOp(OPSTR_READ_MEDIA_VIDEO); 160 denyAppOp(OPSTR_READ_MEDIA_VISUAL_USER_SELECTED); 161 162 grantMediaReadForPackage(sIsolatedContext, sTestAppUid, sUriList); 163 allowAppOp(OPSTR_READ_MEDIA_VISUAL_USER_SELECTED); 164 // verify we can see the grant 165 assertThat(getRowCountForTestPackage()).isEqualTo(sUriList.size()); 166 } 167 168 @Test testAllowVideosOnlyClearsGrants()169 public void testAllowVideosOnlyClearsGrants() throws Exception { 170 // Set the initial state to User Select mode 171 denyAppOp(OPSTR_READ_MEDIA_IMAGES); 172 denyAppOp(OPSTR_READ_MEDIA_VIDEO); 173 allowAppOp(OPSTR_READ_MEDIA_VISUAL_USER_SELECTED); 174 175 grantMediaReadForPackage(sIsolatedContext, sTestAppUid, sUriList); 176 // verify we can see the grant 177 assertThat(getRowCountForTestPackage()).isEqualTo(sUriList.size()); 178 179 // Change the state to Allow All for Videos 180 allowAppOp(OPSTR_READ_MEDIA_VIDEO); 181 assertThat(getRowCountForTestPackage()).isEqualTo(0); 182 } 183 allowAppOp(String op)184 private void allowAppOp(String op) throws InterruptedException { 185 modifyAppOpAndPoll(op, AppOpsManager.MODE_ALLOWED); 186 } 187 denyAppOp(String op)188 private void denyAppOp(String op) throws InterruptedException { 189 modifyAppOpAndPoll(op, AppOpsManager.MODE_ERRORED); 190 } 191 modifyAppOpAndPoll(String op, int mode)192 private void modifyAppOpAndPoll(String op, int mode) 193 throws InterruptedException { 194 sAppOpsManager.startWatchingMode(op, TEST_APP_PACKAGE_NAME, sOnOpChangedListener); 195 synchronized (sLock) { 196 sAppOpsManager.setUidMode(op, sTestAppUid, mode); 197 // Make our best effort to exit early on op change, otherwise wait for 100ms if this was 198 // a no-op change. 199 sLock.wait(100); 200 } 201 sAppOpsManager.stopWatchingMode(sOnOpChangedListener); 202 } 203 getRowCountForTestPackage()204 private int getRowCountForTestPackage() { 205 try (Cursor c = sExternalDatabase.runWithTransaction( 206 (db) -> db.query(MediaGrants.MEDIA_GRANTS_TABLE, 207 new String[]{MediaGrants.FILE_ID_COLUMN, 208 MediaGrants.OWNER_PACKAGE_NAME_COLUMN}, 209 String.format("%s = '%s'", 210 MediaGrants.OWNER_PACKAGE_NAME_COLUMN, TEST_APP_PACKAGE_NAME), 211 null, null, null, null))) { 212 assertWithMessage("Expected cursor to be not null").that(c).isNotNull(); 213 return c.getCount(); 214 } 215 } 216 } 217