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