• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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