• 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 android.scopedstorage.cts.lib;
18 
19 import static android.provider.MediaStore.VOLUME_EXTERNAL;
20 import static android.scopedstorage.cts.lib.RedactionTestHelper.EXIF_METADATA_QUERY;
21 
22 import static androidx.test.InstrumentationRegistry.getContext;
23 
24 import static com.google.common.truth.Truth.assertThat;
25 import static com.google.common.truth.Truth.assertWithMessage;
26 
27 import static junit.framework.Assert.assertEquals;
28 import static junit.framework.TestCase.assertNotNull;
29 
30 import static org.junit.Assert.assertNotEquals;
31 import static org.junit.Assert.fail;
32 
33 import android.Manifest;
34 import android.app.Activity;
35 import android.app.ActivityManager;
36 import android.app.AppOpsManager;
37 import android.app.Instrumentation;
38 import android.app.PendingIntent;
39 import android.app.RecoverableSecurityException;
40 import android.app.UiAutomation;
41 import android.content.BroadcastReceiver;
42 import android.content.ContentResolver;
43 import android.content.ContentUris;
44 import android.content.ContentValues;
45 import android.content.Context;
46 import android.content.Intent;
47 import android.content.IntentFilter;
48 import android.content.pm.PackageManager;
49 import android.database.Cursor;
50 import android.net.Uri;
51 import android.os.Bundle;
52 import android.os.Environment;
53 import android.os.IBinder;
54 import android.os.ParcelFileDescriptor;
55 import android.os.Process;
56 import android.os.storage.StorageManager;
57 import android.provider.MediaStore;
58 import android.system.ErrnoException;
59 import android.system.Os;
60 import android.system.OsConstants;
61 import android.text.TextUtils;
62 import android.util.Log;
63 
64 import androidx.annotation.NonNull;
65 import androidx.annotation.Nullable;
66 import androidx.core.os.BuildCompat;
67 import androidx.test.InstrumentationRegistry;
68 import androidx.test.uiautomator.UiDevice;
69 import androidx.test.uiautomator.UiObject;
70 import androidx.test.uiautomator.UiObjectNotFoundException;
71 import androidx.test.uiautomator.UiScrollable;
72 import androidx.test.uiautomator.UiSelector;
73 
74 import com.android.cts.install.lib.Install;
75 import com.android.cts.install.lib.InstallUtils;
76 import com.android.cts.install.lib.TestApp;
77 import com.android.cts.install.lib.Uninstall;
78 import com.android.modules.utils.build.SdkLevel;
79 
80 import com.google.common.io.ByteStreams;
81 
82 import org.junit.Assert;
83 
84 import java.io.File;
85 import java.io.FileDescriptor;
86 import java.io.FileInputStream;
87 import java.io.IOException;
88 import java.io.InputStream;
89 import java.io.InterruptedIOException;
90 import java.util.ArrayList;
91 import java.util.Arrays;
92 import java.util.HashMap;
93 import java.util.List;
94 import java.util.Locale;
95 import java.util.Optional;
96 import java.util.concurrent.CountDownLatch;
97 import java.util.concurrent.TimeUnit;
98 import java.util.concurrent.TimeoutException;
99 import java.util.function.Supplier;
100 
101 /**
102  * General helper functions for ScopedStorageTest tests.
103  */
104 public class TestUtils {
105     static final String TAG = "ScopedStorageTest";
106 
107     public static final String QUERY_TYPE = "android.scopedstorage.cts.queryType";
108     public static final String INTENT_EXTRA_PATH = "android.scopedstorage.cts.path";
109     public static final String INTENT_EXTRA_CONTENT = "android.scopedstorage.cts.content";
110     public static final String INTENT_EXTRA_URI = "android.scopedstorage.cts.uri";
111     public static final String INTENT_EXTRA_CALLING_PKG = "android.scopedstorage.cts.calling_pkg";
112     public static final String INTENT_EXTRA_ARGS = "android.scopedstorage.cts.args";
113     public static final String INTENT_EXCEPTION = "android.scopedstorage.cts.exception";
114     public static final String FILE_EXISTS_QUERY = "android.scopedstorage.cts.file_exists";
115     public static final String CREATE_FILE_QUERY = "android.scopedstorage.cts.createfile";
116     public static final String CREATE_IMAGE_ENTRY_QUERY =
117             "android.scopedstorage.cts.createimageentry";
118     public static final String DELETE_FILE_QUERY = "android.scopedstorage.cts.deletefile";
119     public static final String DELETE_MEDIA_BY_URI_QUERY =
120             "android.scopedstorage.cts.deletemediabyuri";
121     public static final String DELETE_RECURSIVE_QUERY = "android.scopedstorage.cts.deleteRecursive";
122     public static final String CAN_OPEN_FILE_FOR_READ_QUERY =
123             "android.scopedstorage.cts.can_openfile_read";
124     public static final String CAN_OPEN_FILE_FOR_WRITE_QUERY =
125             "android.scopedstorage.cts.can_openfile_write";
126     public static final String IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_READ =
127             "android.scopedstorage.cts.is_uri_redacted_via_file_descriptor_for_read";
128     public static final String IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_WRITE =
129             "android.scopedstorage.cts.is_uri_redacted_via_file_descriptor_for_write";
130     public static final String IS_URI_REDACTED_VIA_FILEPATH =
131             "android.scopedstorage.cts.is_uri_redacted_via_filepath";
132     public static final String QUERY_URI = "android.scopedstorage.cts.query_uri";
133     public static final String QUERY_MAX_ROW_ID = "android.scopedstorage.cts.query_max_row_id";
134     public static final String QUERY_MIN_ROW_ID = "android.scopedstorage.cts.query_min_row_id";
135     public static final String QUERY_OWNER_PACKAGE_NAMES =
136             "android.scopedstorage.cts.query_owner_package_names";
137     public static final String QUERY_WITH_ARGS = "android.scopedstorage.cts.query_with_args";
138     public static final String OPEN_FILE_FOR_READ_QUERY =
139             "android.scopedstorage.cts.openfile_read";
140     public static final String OPEN_FILE_FOR_WRITE_QUERY =
141             "android.scopedstorage.cts.openfile_write";
142     public static final String CAN_READ_WRITE_QUERY =
143             "android.scopedstorage.cts.can_read_and_write";
144     public static final String READDIR_QUERY = "android.scopedstorage.cts.readdir";
145     public static final String SETATTR_QUERY = "android.scopedstorage.cts.setattr";
146     public static final String CHECK_DATABASE_ROW_EXISTS_QUERY =
147             "android.scopedstorage.cts.check_database_row_exists";
148     public static final String RENAME_FILE_QUERY = "android.scopedstorage.cts.renamefile";
149 
150     public static final String STR_DATA1 = "Just some random text";
151     public static final String STR_DATA2 = "More arbitrary stuff";
152 
153     public static final byte[] BYTES_DATA1 = STR_DATA1.getBytes();
154     public static final byte[] BYTES_DATA2 = STR_DATA2.getBytes();
155 
156     public static final String RENAME_FILE_PARAMS_SEPARATOR = ";";
157 
158     // Root of external storage
159     private static File sExternalStorageDirectory = Environment.getExternalStorageDirectory();
160     private static String sStorageVolumeName = MediaStore.VOLUME_EXTERNAL;
161 
162     /**
163      * Set this to {@code false} if the test is verifying uri grants on testApp. Force stopping the
164      * app will kill the app and it will lose uri grants.
165      */
166     private static boolean sShouldForceStopTestApp = true;
167 
168     private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(20);
169     private static final long POLLING_SLEEP_MILLIS = 100;
170 
171     /**
172      * Creates the top level default directories.
173      *
174      * <p>Those are usually created by MediaProvider, but some naughty tests might delete them
175      * and not restore them afterwards, so we make sure we create them before we make any
176      * assumptions about their existence.
177      */
setupDefaultDirectories()178     public static void setupDefaultDirectories() {
179         for (File dir : getDefaultTopLevelDirs()) {
180             dir.mkdirs();
181             assertWithMessage("Could not setup default dir [%s]", dir.toString())
182                     .that(dir.exists())
183                     .isTrue();
184         }
185     }
186 
187     /**
188      * Grants {@link Manifest.permission#GRANT_RUNTIME_PERMISSIONS} to the given package.
189      */
grantPermission(String packageName, String permission)190     public static void grantPermission(String packageName, String permission) {
191         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
192         uiAutomation.adoptShellPermissionIdentity("android.permission.GRANT_RUNTIME_PERMISSIONS");
193         try {
194             uiAutomation.grantRuntimePermission(packageName, permission);
195         } finally {
196             uiAutomation.dropShellPermissionIdentity();
197         }
198         try {
199             pollForPermission(packageName, permission, true);
200         } catch (Exception e) {
201             fail("Exception on polling for permission grant for " + packageName + " for "
202                     + permission + ": " + e.getMessage());
203         }
204     }
205 
206     /**
207      * Revokes permissions from the given package.
208      */
revokePermission(String packageName, String permission)209     public static void revokePermission(String packageName, String permission) {
210         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
211         uiAutomation.adoptShellPermissionIdentity("android.permission.REVOKE_RUNTIME_PERMISSIONS");
212         try {
213             uiAutomation.revokeRuntimePermission(packageName, permission);
214         } finally {
215             uiAutomation.dropShellPermissionIdentity();
216         }
217         try {
218             pollForPermission(packageName, permission, false);
219         } catch (Exception e) {
220             fail("Exception on polling for permission revoke for " + packageName + " for "
221                     + permission + ": " + e.getMessage());
222         }
223     }
224 
revokeAccessMediaLocation()225     public static void revokeAccessMediaLocation() {
226         revokeAppOpPermission(Manifest.permission.ACCESS_MEDIA_LOCATION,
227                 "android:access_media_location");
228     }
229 
230     /**
231      * Revoke the app op for the given permission. Unlike
232      * {@link TestUtils#revokePermission(String, String)}, its usage does not kill the application.
233      * It can be used to drop permissions previously granted to the test application, without
234      * crashing the test application itself.
235      */
revokeAppOpPermission(String manifestPermission, String appOp)236     private static void revokeAppOpPermission(String manifestPermission, String appOp) {
237         try {
238             androidx.test.platform.app.InstrumentationRegistry.getInstrumentation()
239                     .getUiAutomation()
240                     .adoptShellPermissionIdentity("android.permission.MANAGE_APP_OPS_MODES",
241                             "android.permission.REVOKE_RUNTIME_PERMISSIONS");
242             Context context =
243                     androidx.test.platform.app.InstrumentationRegistry.getInstrumentation()
244                             .getTargetContext();
245             // Revoking the manifest permission will kill the test app.
246             // Deny the permission App Op to revoke this permission.
247             PackageManager packageManager = context.getPackageManager();
248             String packageName = context.getPackageName();
249             if (packageManager.checkPermission(manifestPermission,
250                     packageName) == PackageManager.PERMISSION_GRANTED) {
251                 context.getPackageManager().updatePermissionFlags(
252                         manifestPermission, packageName,
253                         PackageManager.FLAG_PERMISSION_REVOKED_COMPAT,
254                         PackageManager.FLAG_PERMISSION_REVOKED_COMPAT, context.getUser());
255                 context.getSystemService(AppOpsManager.class).setUidMode(
256                         appOp, Process.myUid(),
257                         AppOpsManager.MODE_IGNORED);
258             }
259         } finally {
260             androidx.test.platform.app.InstrumentationRegistry.getInstrumentation()
261                     .getUiAutomation()
262                     .dropShellPermissionIdentity();
263         }
264     }
265 
266     /**
267      * Adopts shell permission identity for the given permissions.
268      */
adoptShellPermissionIdentity(String... permissions)269     public static void adoptShellPermissionIdentity(String... permissions) {
270         InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
271                 permissions);
272     }
273 
274     /**
275      * Drops shell permission identity for all permissions.
276      */
dropShellPermissionIdentity()277     public static void dropShellPermissionIdentity() {
278         InstrumentationRegistry.getInstrumentation().getUiAutomation()
279                 .dropShellPermissionIdentity();
280     }
281 
282     /**
283      * Executes a shell command.
284      */
executeShellCommand(String pattern, Object...args)285     public static String executeShellCommand(String pattern, Object...args) throws IOException {
286         String command = String.format(pattern, args);
287         int attempt = 0;
288         while (attempt++ < 5) {
289             try {
290                 return executeShellCommandInternal(command);
291             } catch (InterruptedIOException e) {
292                 // Hmm, we had trouble executing the shell command; the best we
293                 // can do is try again a few more times
294                 Log.v(TAG, "Trouble executing " + command + "; trying again", e);
295             }
296         }
297         throw new IOException("Failed to execute " + command);
298     }
299 
executeShellCommandInternal(String cmd)300     private static String executeShellCommandInternal(String cmd) throws IOException {
301         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
302         try (FileInputStream output = new FileInputStream(
303                      uiAutomation.executeShellCommand(cmd).getFileDescriptor())) {
304             return new String(ByteStreams.toByteArray(output));
305         }
306     }
307 
308     /**
309      * Makes the given {@code testApp} list the content of the given directory and returns the
310      * result as an {@link ArrayList}
311      */
listAs(TestApp testApp, String dirPath)312     public static ArrayList<String> listAs(TestApp testApp, String dirPath) throws Exception {
313         return getContentsFromTestApp(testApp, dirPath, READDIR_QUERY);
314     }
315 
316     /**
317      * Returns {@code true} iff the given {@code path} exists and is readable and
318      * writable for for {@code testApp}.
319      */
canReadAndWriteAs(TestApp testApp, String path)320     public static boolean canReadAndWriteAs(TestApp testApp, String path) throws Exception {
321         return getResultFromTestApp(testApp, path, CAN_READ_WRITE_QUERY);
322     }
323 
324     /**
325      * Makes the given {@code testApp} read the EXIF metadata from the given file and returns the
326      * result as an {@link HashMap}
327      */
readExifMetadataFromTestApp( TestApp testApp, String filePath)328     public static HashMap<String, String> readExifMetadataFromTestApp(
329             TestApp testApp, String filePath) throws Exception {
330         HashMap<String, String> res =
331                 getMetadataFromTestApp(testApp, filePath, EXIF_METADATA_QUERY);
332         return res;
333     }
334 
335     /**
336      * Makes the given {@code testApp} create a file.
337      *
338      * <p>This method drops shell permission identity.
339      */
createFileAs(TestApp testApp, String path)340     public static boolean createFileAs(TestApp testApp, String path) throws Exception {
341         return getResultFromTestApp(testApp, path, CREATE_FILE_QUERY);
342     }
343 
344     /**
345      * Makes the given {@code testApp} create a file from the file descriptor passed through binder
346      *
347      * <p>This method drops shell permission identity.
348      */
createFileAs(TestApp testApp, String path, IBinder content)349     public static boolean createFileAs(TestApp testApp, String path, IBinder content)
350             throws Exception {
351         return getResultFromTestApp(testApp, path, CREATE_FILE_QUERY, content);
352     }
353 
354     /**
355      * Makes the given {@code testApp} create a mediastore DB entry under
356      * {@code MediaStore.Media.Images}.
357      *
358      * The {@code path} argument is treated as a relative path and a name separated
359      * by an {@code '/'}.
360      */
createImageEntryAs(TestApp testApp, String path)361     public static boolean createImageEntryAs(TestApp testApp, String path) throws Exception {
362         return createImageEntryForUriAs(testApp, path) != null;
363     }
364 
365     /**
366      * Makes the given {@code testApp} create a mediastore DB entry under
367      * {@code MediaStore.Media.Images}.
368      *
369      * The {@code path} argument is treated as a relative path and a name separated
370      * by an {@code '/'}.
371      *
372      * Returns URI of the created image.
373      */
createImageEntryForUriAs(TestApp testApp, String path)374     public static Uri createImageEntryForUriAs(TestApp testApp, String path) throws Exception {
375         final String actionName = CREATE_IMAGE_ENTRY_QUERY;
376         final String uriString = getFromTestApp(testApp, path, actionName)
377                 .getString(actionName, null);
378         return Uri.parse(uriString);
379     }
380 
381     /**
382      * Makes the given {@code testApp} query on {@code uri} to get all the ownerPackageName values.
383      *
384      * <p>This method drops shell permission identity.
385      */
queryForOwnerPackageNamesAs(TestApp testApp, Uri uri)386     public static String[] queryForOwnerPackageNamesAs(TestApp testApp, Uri uri) throws Exception {
387         final String actionName = QUERY_OWNER_PACKAGE_NAMES;
388         return getFromTestApp(testApp, uri, actionName).getStringArray(actionName);
389     }
390 
391     /**
392      * Makes the given {@code testApp} query on {@code uri} with the provided {@code queryArgs}.
393      *
394      * Returns the number of rows in the result cursor.
395      *
396      * <p>This method drops shell permission identity.
397      */
queryWithArgsAs(TestApp testApp, Uri uri, Bundle queryArgs)398     public static int queryWithArgsAs(TestApp testApp, Uri uri, Bundle queryArgs) throws Exception {
399         final String actionName = QUERY_WITH_ARGS;
400         return getFromTestApp(testApp, uri, actionName, queryArgs).getInt(actionName);
401     }
402 
403     /**
404      * Makes the given {@code testApp} delete media rows by the provided {@code uri}.
405      *
406      * Returns the number of deleted rows.
407      *
408      * <p>This method drops shell permission identity.
409      */
deleteMediaByUriAs(TestApp testApp, Uri uri)410     public static int deleteMediaByUriAs(TestApp testApp, Uri uri) throws Exception {
411         final String actionName = DELETE_MEDIA_BY_URI_QUERY;
412         return getFromTestApp(testApp, uri, actionName).getInt(actionName);
413     }
414 
415     /**
416      * Makes the given {@code testApp} delete a file.
417      *
418      * <p>This method drops shell permission identity.
419      */
deleteFileAs(TestApp testApp, String path)420     public static boolean deleteFileAs(TestApp testApp, String path) throws Exception {
421         return getResultFromTestApp(testApp, path, DELETE_FILE_QUERY);
422     }
423 
424     /**
425      * Makes the given {@code testApp} delete a file or directory.
426      * If the file is a directory, then deletes all of its children (file or directories)
427      * recursively.
428      *
429      * <p>This method drops shell permission identity.
430      */
deleteRecursivelyAs(TestApp testApp, String path)431     public static boolean deleteRecursivelyAs(TestApp testApp, String path) throws Exception {
432         return getResultFromTestApp(testApp, path, DELETE_RECURSIVE_QUERY);
433     }
434 
435     /**
436      * Makes the given {@code testApp} delete a file. Doesn't throw in case of failure.
437      */
deleteFileAsNoThrow(TestApp testApp, String path)438     public static boolean deleteFileAsNoThrow(TestApp testApp, String path) {
439         try {
440             return deleteFileAs(testApp, path);
441         } catch (Exception e) {
442             Log.e(TAG,
443                     "Error occurred while deleting file: " + path + " on behalf of app: " + testApp,
444                     e);
445             return false;
446         }
447     }
448 
449     /**
450      * Makes the given {@code testApp} test {@code file} for existence.
451      *
452      * <p>This method drops shell permission identity.
453      */
fileExistsAs(TestApp testApp, File file)454     public static boolean fileExistsAs(TestApp testApp, File file)
455             throws Exception {
456         return getResultFromTestApp(testApp, file.getPath(), FILE_EXISTS_QUERY);
457     }
458 
459     /**
460      * Makes the given {@code testApp} open {@code file} for read or write.
461      *
462      * <p>This method drops shell permission identity.
463      */
canOpenFileAs(TestApp testApp, File file, boolean forWrite)464     public static boolean canOpenFileAs(TestApp testApp, File file, boolean forWrite)
465             throws Exception {
466         String actionName = forWrite ? CAN_OPEN_FILE_FOR_WRITE_QUERY : CAN_OPEN_FILE_FOR_READ_QUERY;
467         return getResultFromTestApp(testApp, file.getPath(), actionName);
468     }
469 
470     /**
471      * Makes the given {@code testApp} rename give {@code src} to {@code dst}.
472      *
473      * The method concatenates source and destination paths while sending the request to
474      * {@code testApp}. Hence, {@link TestUtils#RENAME_FILE_PARAMS_SEPARATOR} shouldn't be used
475      * in path names.
476      *
477      * <p>This method drops shell permission identity.
478      */
renameFileAs(TestApp testApp, File src, File dst)479     public static boolean renameFileAs(TestApp testApp, File src, File dst) throws Exception {
480         final String paths = String.format("%s%s%s",
481                 src.getAbsolutePath(), RENAME_FILE_PARAMS_SEPARATOR, dst.getAbsolutePath());
482         return getResultFromTestApp(testApp, paths, RENAME_FILE_QUERY);
483     }
484 
485     /**
486      * Makes the given {@code testApp} check if a database row exists for given {@code file}
487      *
488      * <p>This method drops shell permission identity.
489      */
checkDatabaseRowExistsAs(TestApp testApp, File file)490     public static boolean checkDatabaseRowExistsAs(TestApp testApp, File file) throws Exception {
491         return getResultFromTestApp(testApp, file.getPath(), CHECK_DATABASE_ROW_EXISTS_QUERY);
492     }
493 
494     /**
495      * Makes the given {@code testApp} open file descriptor on {@code uri} and verifies that the fd
496      * redacts EXIF metadata.
497      *
498      * <p> This method drops shell permission identity.
499      */
isFileDescriptorRedacted(TestApp testApp, Uri uri)500     public static boolean isFileDescriptorRedacted(TestApp testApp, Uri uri)
501             throws Exception {
502         String actionName = IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_READ;
503         return getFromTestApp(testApp, uri, actionName).getBoolean(actionName, false);
504     }
505 
506     /**
507      * Makes the given {@code testApp} open file descriptor on {@code uri} and verifies that the fd
508      * redacts EXIF metadata.
509      *
510      * <p> This method drops shell permission identity.
511      */
canOpenRedactedUriForWrite(TestApp testApp, Uri uri)512     public static boolean canOpenRedactedUriForWrite(TestApp testApp, Uri uri)
513             throws Exception {
514         String actionName = IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_WRITE;
515         return getFromTestApp(testApp, uri, actionName).getBoolean(actionName, false);
516     }
517 
518 
519     /**
520      * Makes the given {@code testApp} open file path associated with {@code uri} and verifies that
521      * the path redacts EXIF metadata.
522      *
523      * <p>This method drops shell permission identity.
524      */
isFileOpenRedacted(TestApp testApp, Uri uri)525     public static boolean isFileOpenRedacted(TestApp testApp, Uri uri)
526             throws Exception {
527         final String actionName = IS_URI_REDACTED_VIA_FILEPATH;
528         return getFromTestApp(testApp, uri, actionName).getBoolean(actionName, false);
529     }
530 
531     /**
532      * Makes the given {@code testApp} query on {@code uri}.
533      *
534      * <p>This method drops shell permission identity.
535      */
canQueryOnUri(TestApp testApp, Uri uri)536     public static boolean canQueryOnUri(TestApp testApp, Uri uri) throws Exception {
537         final String actionName = QUERY_URI;
538         return getFromTestApp(testApp, uri, actionName).getBoolean(actionName, false);
539     }
540 
insertFileFromExternalMedia(boolean useRelative)541     public static Uri insertFileFromExternalMedia(boolean useRelative) throws IOException {
542         ContentValues values = new ContentValues();
543         String filePath =
544                 getAndroidMediaDir().toString() + "/" + getContext().getPackageName() + "/"
545                         + System.currentTimeMillis();
546         if (useRelative) {
547             values.put(MediaStore.MediaColumns.RELATIVE_PATH,
548                     "Android/media/" + getContext().getPackageName());
549             values.put(MediaStore.MediaColumns.DISPLAY_NAME, System.currentTimeMillis());
550         } else {
551             values.put(MediaStore.MediaColumns.DATA, filePath);
552         }
553 
554         return getContentResolver().insert(
555                 MediaStore.Files.getContentUri(sStorageVolumeName), values);
556     }
557 
insertFile(ContentValues values)558     public static void insertFile(ContentValues values) {
559         assertNotNull(getContentResolver().insert(
560                 MediaStore.Files.getContentUri(sStorageVolumeName), values));
561     }
562 
updateFile(Uri uri, ContentValues values)563     public static int updateFile(Uri uri, ContentValues values) {
564         return getContentResolver().update(uri, values, new Bundle());
565     }
566 
verifyInsertFromExternalPrivateDirViaRelativePath_denied()567     public static void verifyInsertFromExternalPrivateDirViaRelativePath_denied() throws Exception {
568         // Test that inserting files from Android/obb/.. is not allowed.
569         final String androidObbDir = getExternalObbDir().toString();
570         ContentValues values = new ContentValues();
571         values.put(
572                 MediaStore.MediaColumns.RELATIVE_PATH,
573                 androidObbDir.substring(androidObbDir.indexOf("Android")));
574         assertThrows(IllegalArgumentException.class, () -> insertFile(values));
575 
576         // Test that inserting files from Android/data/.. is not allowed.
577         final String androidDataDir = getExternalFilesDir().toString();
578         values.put(
579                 MediaStore.MediaColumns.RELATIVE_PATH,
580                 androidDataDir.substring(androidDataDir.indexOf("Android")));
581         assertThrows(IllegalArgumentException.class, () -> insertFile(values));
582     }
583 
verifyInsertFromExternalMediaDirViaRelativePath_allowed()584     public static void verifyInsertFromExternalMediaDirViaRelativePath_allowed() throws Exception {
585         // Test that inserting files from Android/media/.. is allowed.
586         final String androidMediaDir = getExternalMediaDir().toString();
587         final ContentValues values = new ContentValues();
588         values.put(
589                 MediaStore.MediaColumns.RELATIVE_PATH,
590                 androidMediaDir.substring(androidMediaDir.indexOf("Android")));
591         insertFile(values);
592     }
593 
verifyInsertFromExternalPrivateDirViaData_denied()594     public static void verifyInsertFromExternalPrivateDirViaData_denied() throws Exception {
595         ContentValues values = new ContentValues();
596 
597         // Test that inserting files from Android/obb/.. is not allowed.
598         final String androidObbDir =
599                 getExternalObbDir().toString() + "/" + System.currentTimeMillis();
600         values.put(MediaStore.MediaColumns.DATA, androidObbDir);
601         assertThrows(IllegalArgumentException.class, () -> insertFile(values));
602 
603         // Test that inserting files from Android/data/.. is not allowed.
604         final String androidDataDir = getExternalFilesDir().toString();
605         values.put(MediaStore.MediaColumns.DATA, androidDataDir);
606         assertThrows(IllegalArgumentException.class, () -> insertFile(values));
607     }
608 
verifyInsertFromExternalMediaDirViaData_allowed()609     public static void verifyInsertFromExternalMediaDirViaData_allowed() throws Exception {
610         // Test that inserting files from Android/media/.. is allowed.
611         ContentValues values = new ContentValues();
612         final String androidMediaDirFile =
613                 getExternalMediaDir().toString() + "/" + System.currentTimeMillis();
614         values.put(MediaStore.MediaColumns.DATA, androidMediaDirFile);
615         insertFile(values);
616     }
617 
618     // NOTE: While updating, DATA field should be ignored for all the apps including file manager.
verifyUpdateToExternalDirsViaData_denied()619     public static void verifyUpdateToExternalDirsViaData_denied() throws Exception {
620         Uri uri = insertFileFromExternalMedia(false);
621 
622         final String androidMediaDirFile =
623                 getExternalMediaDir().toString() + "/" + System.currentTimeMillis();
624         ContentValues values = new ContentValues();
625         values.put(MediaStore.MediaColumns.DATA, androidMediaDirFile);
626         assertEquals(0, updateFile(uri, values));
627 
628         final String androidObbDir =
629                 getExternalObbDir().toString() + "/" + System.currentTimeMillis();
630         values.put(MediaStore.MediaColumns.DATA, androidObbDir);
631         assertEquals(0, updateFile(uri, values));
632 
633         final String androidDataDir = getExternalFilesDir().toString();
634         values.put(MediaStore.MediaColumns.DATA, androidDataDir);
635         assertEquals(0, updateFile(uri, values));
636     }
637 
verifyUpdateToExternalMediaDirViaRelativePath_allowed()638     public static void verifyUpdateToExternalMediaDirViaRelativePath_allowed()
639             throws IOException {
640         Uri uri = insertFileFromExternalMedia(true);
641 
642         // Test that update to files from Android/media/.. is allowed.
643         final String androidMediaDir = getExternalMediaDir().toString();
644         ContentValues values = new ContentValues();
645         values.put(
646                 MediaStore.MediaColumns.RELATIVE_PATH,
647                 androidMediaDir.substring(androidMediaDir.indexOf("Android")));
648         assertNotEquals(0, updateFile(uri, values));
649     }
650 
verifyUpdateToExternalPrivateDirsViaRelativePath_denied()651     public static void verifyUpdateToExternalPrivateDirsViaRelativePath_denied()
652             throws Exception {
653         Uri uri = insertFileFromExternalMedia(true);
654 
655         // Test that update to files from Android/obb/.. is not allowed.
656         final String androidObbDir = getExternalObbDir().toString();
657         ContentValues values = new ContentValues();
658         values.put(
659                 MediaStore.MediaColumns.RELATIVE_PATH,
660                 androidObbDir.substring(androidObbDir.indexOf("Android")));
661         assertThrows(IllegalArgumentException.class, () -> updateFile(uri, values));
662 
663         // Test that update to files from Android/data/.. is not allowed.
664         final String androidDataDir = getExternalFilesDir().toString();
665         values.put(
666                 MediaStore.MediaColumns.RELATIVE_PATH,
667                 androidDataDir.substring(androidDataDir.indexOf("Android")));
668         assertThrows(IllegalArgumentException.class, () -> updateFile(uri, values));
669     }
670 
671     /**
672      * Makes the given {@code testApp} open a file for read or write.
673      *
674      * <p>This method drops shell permission identity.
675      */
openFileAs(TestApp testApp, File file, boolean forWrite)676     public static ParcelFileDescriptor openFileAs(TestApp testApp, File file, boolean forWrite)
677             throws Exception {
678         String actionName = forWrite ? OPEN_FILE_FOR_WRITE_QUERY : OPEN_FILE_FOR_READ_QUERY;
679         String mode = forWrite ? "rw" : "r";
680         return getPfdFromTestApp(testApp, file, actionName, mode);
681     }
682 
683     /**
684      * Makes the given {@code testApp} setattr for given file path.
685      *
686      * <p>This method drops shell permission identity.
687      */
setAttrAs(TestApp testApp, String path)688     public static boolean setAttrAs(TestApp testApp, String path)
689             throws Exception {
690         return getResultFromTestApp(testApp, path, SETATTR_QUERY);
691     }
692 
693     /**
694      * Installs a {@link TestApp} without storage permissions.
695      */
installApp(TestApp testApp)696     public static void installApp(TestApp testApp) throws Exception {
697         installApp(testApp, /* grantStoragePermission */ false);
698     }
699 
700     /**
701      * Installs a {@link TestApp} with storage permissions.
702      */
installAppWithStoragePermissions(TestApp testApp)703     public static void installAppWithStoragePermissions(TestApp testApp) throws Exception {
704         installApp(testApp, /* grantStoragePermission */ true);
705     }
706 
707     /**
708      * Installs a {@link TestApp} and may grant it storage permissions.
709      */
installApp(TestApp testApp, boolean grantStoragePermission)710     public static void installApp(TestApp testApp, boolean grantStoragePermission)
711             throws Exception {
712         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
713         try {
714             final String packageName = testApp.getPackageName();
715             uiAutomation.adoptShellPermissionIdentity(
716                     Manifest.permission.INSTALL_PACKAGES, Manifest.permission.DELETE_PACKAGES);
717             if (isAppInstalled(testApp)) {
718                 Uninstall.packages(packageName);
719             }
720             Install.single(testApp).commit();
721             assertThat(InstallUtils.getInstalledVersion(packageName)).isEqualTo(1);
722             if (grantStoragePermission) {
723                 grantPermission(packageName, Manifest.permission.READ_EXTERNAL_STORAGE);
724                 if (SdkLevel.isAtLeastT()) {
725                     grantPermission(packageName, Manifest.permission.READ_MEDIA_IMAGES);
726                     grantPermission(packageName, Manifest.permission.READ_MEDIA_AUDIO);
727                     grantPermission(packageName, Manifest.permission.READ_MEDIA_VIDEO);
728                 }
729             }
730         } finally {
731             uiAutomation.dropShellPermissionIdentity();
732         }
733     }
734 
isAppInstalled(TestApp testApp)735     public static boolean isAppInstalled(TestApp testApp) {
736         return InstallUtils.getInstalledVersion(testApp.getPackageName()) != -1;
737     }
738 
739     /**
740      * Uninstalls a {@link TestApp}.
741      */
uninstallApp(TestApp testApp)742     public static void uninstallApp(TestApp testApp) throws Exception {
743         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
744         try {
745             final String packageName = testApp.getPackageName();
746             uiAutomation.adoptShellPermissionIdentity(Manifest.permission.DELETE_PACKAGES);
747 
748             Uninstall.packages(packageName);
749             assertThat(InstallUtils.getInstalledVersion(packageName)).isEqualTo(-1);
750         } finally {
751             uiAutomation.dropShellPermissionIdentity();
752         }
753     }
754 
755     /**
756      * Uninstalls a {@link TestApp}. Doesn't throw in case of failure.
757      */
uninstallAppNoThrow(TestApp testApp)758     public static void uninstallAppNoThrow(TestApp testApp) {
759         try {
760             uninstallApp(testApp);
761         } catch (Exception e) {
762             Log.e(TAG, "Exception occurred while uninstalling app: " + testApp, e);
763         }
764     }
765 
getContentResolver()766     public static ContentResolver getContentResolver() {
767         return getContext().getContentResolver();
768     }
769 
770     /**
771      * Inserts a file into the database using {@link MediaStore.MediaColumns#DATA}.
772      */
insertFileUsingDataColumn(@onNull File file)773     public static Uri insertFileUsingDataColumn(@NonNull File file) {
774         final ContentValues values = new ContentValues();
775         values.put(MediaStore.MediaColumns.DATA, file.getPath());
776         return getContentResolver().insert(MediaStore.Files.getContentUri(sStorageVolumeName),
777                 values);
778     }
779 
780     /**
781      * Returns the content URI for images based on the current storage volume.
782      */
getImageContentUri()783     public static Uri getImageContentUri() {
784         return MediaStore.Images.Media.getContentUri(sStorageVolumeName);
785     }
786 
787     /**
788      * Returns the content URI for videos based on the current storage volume.
789      */
getVideoContentUri()790     public static Uri getVideoContentUri() {
791         return MediaStore.Video.Media.getContentUri(sStorageVolumeName);
792     }
793 
794     /**
795      * Renames the given file using {@link ContentResolver} and {@link MediaStore} and APIs.
796      * This method uses the data column, and not all apps can use it.
797      *
798      * @see MediaStore.MediaColumns#DATA
799      */
renameWithMediaProvider(@onNull File oldPath, @NonNull File newPath)800     public static int renameWithMediaProvider(@NonNull File oldPath, @NonNull File newPath) {
801         ContentValues values = new ContentValues();
802         values.put(MediaStore.MediaColumns.DATA, newPath.getPath());
803         return getContentResolver().update(MediaStore.Files.getContentUri(sStorageVolumeName),
804                 values, /*where*/ MediaStore.MediaColumns.DATA + "=?",
805                 /*whereArgs*/ new String[]{oldPath.getPath()});
806     }
807 
808     /**
809      * Queries {@link ContentResolver} for a file and returns the corresponding {@link Uri} for its
810      * entry in the database. Returns {@code null} if file doesn't exist in the database.
811      */
812     @Nullable
getFileUri(@onNull File file)813     public static Uri getFileUri(@NonNull File file) {
814         final Uri contentUri = MediaStore.Files.getContentUri(sStorageVolumeName);
815         final int id = getFileRowIdFromDatabase(file);
816         return id == -1 ? null : ContentUris.withAppendedId(contentUri, id);
817     }
818 
819     /**
820      * Queries {@link ContentResolver} for a file and returns the corresponding row ID for its
821      * entry in the database. Returns {@code -1} if file is not found.
822      */
getFileRowIdFromDatabase(@onNull File file)823     public static int getFileRowIdFromDatabase(@NonNull File file) {
824         return getFileRowIdFromDatabase(getContentResolver(), file);
825     }
826 
827     /**
828      * Queries given {@link ContentResolver} for a file and returns the corresponding row ID for
829      * its entry in the database. Returns {@code -1} if file is not found.
830      */
getFileRowIdFromDatabase(ContentResolver cr, @NonNull File file)831     public static int getFileRowIdFromDatabase(ContentResolver cr, @NonNull File file) {
832         int id = -1;
833         try (Cursor c = queryFile(cr, file, MediaStore.MediaColumns._ID)) {
834             if (c.moveToFirst()) {
835                 id = c.getInt(0);
836             }
837         }
838         return id;
839     }
840 
841     /**
842      * Queries {@link ContentResolver} for a file and returns the corresponding owner package name
843      * for its entry in the database.
844      */
845     @Nullable
getFileOwnerPackageFromDatabase(@onNull File file)846     public static String getFileOwnerPackageFromDatabase(@NonNull File file) {
847         String ownerPackage = null;
848         try (Cursor c = queryFile(file, MediaStore.MediaColumns.OWNER_PACKAGE_NAME)) {
849             if (c.moveToFirst()) {
850                 ownerPackage = c.getString(0);
851             }
852         }
853         return ownerPackage;
854     }
855 
856     /**
857      * Queries {@link ContentResolver} for a file and returns the corresponding file size for its
858      * entry in the database. Returns {@code -1} if file is not found.
859      */
860     @Nullable
getFileSizeFromDatabase(@onNull File file)861     public static int getFileSizeFromDatabase(@NonNull File file) {
862         int size = -1;
863         try (Cursor c = queryFile(file, MediaStore.MediaColumns.SIZE)) {
864             if (c.moveToFirst()) {
865                 size = c.getInt(0);
866             }
867         }
868         return size;
869     }
870 
871     /**
872      * Queries {@link ContentResolver} for a video file and returns a {@link Cursor} with the given
873      * columns.
874      */
875     @NonNull
queryVideoFile(File file, String... projection)876     public static Cursor queryVideoFile(File file, String... projection) {
877         return queryFile(getContentResolver(),
878                 MediaStore.Video.Media.getContentUri(sStorageVolumeName), file,
879                 /*includePending*/ true, projection);
880     }
881 
882     /**
883      * Queries {@link ContentResolver} for an image file and returns a {@link Cursor} with the given
884      * columns.
885      */
886     @NonNull
queryImageFile(File file, String... projection)887     public static Cursor queryImageFile(File file, String... projection) {
888         return queryFile(getContentResolver(),
889                 MediaStore.Images.Media.getContentUri(sStorageVolumeName), file,
890                 /*includePending*/ true, projection);
891     }
892 
893     /**
894      * Queries {@link ContentResolver} for an audio file and returns a {@link Cursor} with the given
895      * columns.
896      */
897     @NonNull
queryAudioFile(File file, String... projection)898     public static Cursor queryAudioFile(File file, String... projection) {
899         return queryFile(getContentResolver(),
900                 MediaStore.Audio.Media.getContentUri(sStorageVolumeName), file,
901                 /*includePending*/ true, projection);
902     }
903 
904     /**
905      * Queries {@link ContentResolver} for a file and returns the corresponding mime type for its
906      * entry in the database.
907      */
908     @NonNull
getFileMimeTypeFromDatabase(@onNull File file)909     public static String getFileMimeTypeFromDatabase(@NonNull File file) {
910         String mimeType = "";
911         try (Cursor c = queryFile(file, MediaStore.MediaColumns.MIME_TYPE)) {
912             if (c.moveToFirst()) {
913                 mimeType = c.getString(0);
914             }
915         }
916         return mimeType;
917     }
918 
919     /**
920      * Sets {@link AppOpsManager#MODE_ALLOWED} for the given {@code ops} and the given {@code uid}.
921      *
922      * <p>This method drops shell permission identity.
923      */
allowAppOpsToUid(int uid, @NonNull String... ops)924     public static void allowAppOpsToUid(int uid, @NonNull String... ops) {
925         setAppOpsModeForUid(uid, AppOpsManager.MODE_ALLOWED, ops);
926     }
927 
928     /**
929      * Sets {@link AppOpsManager#MODE_ERRORED} for the given {@code ops} and the given {@code uid}.
930      *
931      * <p>This method drops shell permission identity.
932      */
denyAppOpsToUid(int uid, @NonNull String... ops)933     public static void denyAppOpsToUid(int uid, @NonNull String... ops) {
934         setAppOpsModeForUid(uid, AppOpsManager.MODE_ERRORED, ops);
935     }
936 
937     /**
938      * Deletes the given file through {@link ContentResolver} and {@link MediaStore} APIs,
939      * and asserts that the file was successfully deleted from the database.
940      */
deleteWithMediaProvider(@onNull File file)941     public static void deleteWithMediaProvider(@NonNull File file) {
942         Bundle extras = new Bundle();
943         extras.putString(ContentResolver.QUERY_ARG_SQL_SELECTION,
944                 MediaStore.MediaColumns.DATA + " = ?");
945         extras.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
946                 new String[]{file.getPath()});
947         extras.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_INCLUDE);
948         extras.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE);
949         assertThat(getContentResolver().delete(
950                 MediaStore.Files.getContentUri(sStorageVolumeName), extras)).isEqualTo(1);
951     }
952 
953     /**
954      * Deletes db rows and files corresponding to uri through {@link ContentResolver} and
955      * {@link MediaStore} APIs.
956      */
deleteWithMediaProviderNoThrow(Uri... uris)957     public static void deleteWithMediaProviderNoThrow(Uri... uris) {
958         for (Uri uri : uris) {
959             if (uri == null) continue;
960 
961             try {
962                 getContentResolver().delete(uri, Bundle.EMPTY);
963             } catch (Exception ignored) {
964             }
965         }
966     }
967 
968     /**
969      * Renames the given file through {@link ContentResolver} and {@link MediaStore} APIs,
970      * and asserts that the file was updated in the database.
971      */
updateDisplayNameWithMediaProvider(Uri uri, String relativePath, String oldDisplayName, String newDisplayName)972     public static void updateDisplayNameWithMediaProvider(Uri uri, String relativePath,
973             String oldDisplayName, String newDisplayName) {
974         String selection = MediaStore.MediaColumns.RELATIVE_PATH + " = ? AND "
975                 + MediaStore.MediaColumns.DISPLAY_NAME + " = ?";
976         String[] selectionArgs = {relativePath + '/', oldDisplayName};
977         Bundle extras = new Bundle();
978         extras.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, selection);
979         extras.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, selectionArgs);
980         extras.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_INCLUDE);
981         extras.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE);
982 
983         ContentValues values = new ContentValues();
984         values.put(MediaStore.MediaColumns.DISPLAY_NAME, newDisplayName);
985 
986         assertThat(getContentResolver().update(uri, values, extras)).isEqualTo(1);
987     }
988 
989     /**
990      * Opens the given file through {@link ContentResolver} and {@link MediaStore} APIs.
991      */
992     @NonNull
openWithMediaProvider(@onNull File file, String mode)993     public static ParcelFileDescriptor openWithMediaProvider(@NonNull File file, String mode)
994             throws Exception {
995         final Uri fileUri = getFileUri(file);
996         assertThat(fileUri).isNotNull();
997         Log.i(TAG, "Uri: " + fileUri + ". Data: " + file.getPath());
998         ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(fileUri, mode);
999         assertThat(pfd).isNotNull();
1000         return pfd;
1001     }
1002 
1003     /**
1004      * Opens the given file via file path
1005      */
1006     @NonNull
openWithFilePath(File file, boolean forWrite)1007     public static ParcelFileDescriptor openWithFilePath(File file, boolean forWrite)
1008             throws IOException {
1009         return ParcelFileDescriptor.open(file,
1010                 forWrite
1011                         ? ParcelFileDescriptor.MODE_READ_WRITE
1012                         : ParcelFileDescriptor.MODE_READ_ONLY);
1013     }
1014 
1015     /**
1016      * Returns whether we can open the file.
1017      */
canOpen(File file, boolean forWrite)1018     public static boolean canOpen(File file, boolean forWrite) {
1019         try (ParcelFileDescriptor ignore = openWithFilePath(file, forWrite)) {
1020             return true;
1021         } catch (IOException expected) {
1022             return false;
1023         }
1024     }
1025 
1026     /**
1027      * Asserts the given operation throws an exception of type {@code T}.
1028      */
assertThrows(Class<T> clazz, Operation<Exception> r)1029     public static <T extends Exception> void assertThrows(Class<T> clazz, Operation<Exception> r)
1030             throws Exception {
1031         assertThrows(clazz, "", r);
1032     }
1033 
1034     /**
1035      * Asserts the given operation throws an exception of type {@code T}.
1036      */
assertThrows( Class<T> clazz, String errMsg, Operation<Exception> r)1037     public static <T extends Exception> void assertThrows(
1038             Class<T> clazz, String errMsg, Operation<Exception> r) throws Exception {
1039         try {
1040             r.run();
1041             fail("Expected " + clazz + " to be thrown");
1042         } catch (Exception e) {
1043             if (!clazz.isAssignableFrom(e.getClass()) || !e.getMessage().contains(errMsg)) {
1044                 Log.e(TAG, "Expected " + clazz + " exception with error message: " + errMsg, e);
1045                 throw e;
1046             }
1047         }
1048     }
1049 
setShouldForceStopTestApp(boolean value)1050     public static void setShouldForceStopTestApp(boolean value) {
1051         sShouldForceStopTestApp = value;
1052     }
1053 
readMaximumRowIdFromDatabaseAs(TestApp app, Uri uri)1054     public static long readMaximumRowIdFromDatabaseAs(TestApp app, Uri uri) throws Exception {
1055         final String actionName = QUERY_MAX_ROW_ID;
1056         return getFromTestApp(app, uri, actionName).getLong(actionName, Long.MIN_VALUE);
1057     }
1058 
readMinimumRowIdFromDatabaseAs(TestApp app, Uri uri)1059     public static long readMinimumRowIdFromDatabaseAs(TestApp app, Uri uri) throws Exception {
1060         final String actionName = QUERY_MIN_ROW_ID;
1061         return getFromTestApp(app, uri, actionName).getLong(actionName, Long.MAX_VALUE);
1062     }
1063 
doEscalation(RecoverableSecurityException exception)1064     public static void doEscalation(RecoverableSecurityException exception) throws Exception {
1065         doEscalation(exception.getUserAction().getActionIntent());
1066     }
1067 
doEscalation(PendingIntent pi)1068     public static void doEscalation(PendingIntent pi) throws Exception {
1069         doEscalation(pi, true /* allowAccess */, false /* shouldCheckDialogShownValue */,
1070                 false /* isDialogShownExpectedExpected */);
1071     }
1072 
doEscalation(PendingIntent pi, boolean allowAccess, boolean shouldCheckDialogShownValue, boolean isDialogShownExpected)1073     public static void doEscalation(PendingIntent pi, boolean allowAccess,
1074             boolean shouldCheckDialogShownValue, boolean isDialogShownExpected) throws Exception {
1075         // Try launching the action to grant ourselves access
1076         final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
1077         final Intent intent = new Intent(inst.getContext(), GetResultActivity.class);
1078         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1079 
1080         // Wake up the device and dismiss the keyguard before the test starts
1081         final UiDevice device = UiDevice.getInstance(inst);
1082         device.executeShellCommand("input keyevent KEYCODE_WAKEUP");
1083         device.executeShellCommand("wm dismiss-keyguard");
1084 
1085         final GetResultActivity activity = (GetResultActivity) inst.startActivitySync(intent);
1086         // Wait for the UI Thread to become idle.
1087         inst.waitForIdleSync();
1088         activity.clearResult();
1089         device.waitForIdle();
1090         activity.startIntentSenderForResult(pi.getIntentSender(), 42, null, 0, 0, 0);
1091 
1092         device.waitForIdle();
1093         final long timeout = 5_000;
1094         if (allowAccess) {
1095             // Some dialogs may have granted access automatically, so we're willing
1096             // to keep rolling forward if we can't find our grant button
1097             final UiSelector grant = new UiSelector().textMatches("(?i)Allow");
1098             if (isWatch(inst.getContext().getPackageManager())) {
1099                 UiScrollable uiScrollable = new UiScrollable(new UiSelector().scrollable(true));
1100                 try {
1101                     uiScrollable.scrollIntoView(grant);
1102                 } catch (UiObjectNotFoundException e) {
1103                     // Scrolling can fail if the UI is not scrollable
1104                 }
1105             }
1106             final boolean grantExists = new UiObject(grant).waitForExists(timeout);
1107 
1108             if (shouldCheckDialogShownValue) {
1109                 assertThat(grantExists).isEqualTo(isDialogShownExpected);
1110             }
1111 
1112             if (grantExists) {
1113                 device.findObject(grant).click();
1114             }
1115             final GetResultActivity.Result res = activity.getResult();
1116             // Verify that we now have access
1117             Assert.assertEquals(Activity.RESULT_OK, res.resultCode);
1118         } else {
1119             // fine the Deny button
1120             final UiSelector deny = new UiSelector().textMatches("(?i)Deny");
1121             final boolean denyExists = new UiObject(deny).waitForExists(timeout);
1122 
1123             assertThat(denyExists).isTrue();
1124 
1125             device.findObject(deny).click();
1126 
1127             final GetResultActivity.Result res = activity.getResult();
1128             // Verify that we don't have access
1129             Assert.assertEquals(Activity.RESULT_CANCELED, res.resultCode);
1130         }
1131     }
1132 
isWatch(PackageManager packageManager)1133     private static boolean isWatch(PackageManager packageManager) {
1134         return hasFeature(packageManager, PackageManager.FEATURE_WATCH);
1135     }
1136 
hasFeature(PackageManager packageManager, String feature)1137     private static boolean hasFeature(PackageManager packageManager, String feature) {
1138         return packageManager.hasSystemFeature(feature);
1139     }
1140 
1141     /**
1142      * A functional interface representing an operation that takes no arguments,
1143      * returns no arguments and might throw an {@link Exception} of any kind.
1144      *
1145      * @param T the subclass of {@link java.lang.Exception} that this operation might throw.
1146      */
1147     @FunctionalInterface
1148     public interface Operation<T extends Exception> {
1149         /**
1150          * This is the method that gets called for any object that implements this interface.
1151          */
run()1152         void run() throws T;
1153     }
1154 
1155     /**
1156      * Deletes the given file. If the file is a directory, then deletes all of its children (files
1157      * or directories) recursively.
1158      */
deleteRecursively(@onNull File path)1159     public static boolean deleteRecursively(@NonNull File path) {
1160         if (path.isDirectory()) {
1161             for (File child : path.listFiles()) {
1162                 if (!deleteRecursively(child)) {
1163                     return false;
1164                 }
1165             }
1166         }
1167         return path.delete();
1168     }
1169 
1170     /**
1171      * Asserts can rename file.
1172      */
assertCanRenameFile(File oldFile, File newFile)1173     public static void assertCanRenameFile(File oldFile, File newFile) {
1174         assertCanRenameFile(oldFile, newFile, /* checkDB */ true);
1175     }
1176 
1177     /**
1178      * Asserts can rename file and optionally checks if the database is updated after rename.
1179      */
assertCanRenameFile(File oldFile, File newFile, boolean checkDatabase)1180     public static void assertCanRenameFile(File oldFile, File newFile, boolean checkDatabase) {
1181         assertThat(oldFile.renameTo(newFile)).isTrue();
1182         assertThat(oldFile.exists()).isFalse();
1183         assertThat(newFile.exists()).isTrue();
1184         if (checkDatabase) {
1185             assertThat(getFileRowIdFromDatabase(oldFile)).isEqualTo(-1);
1186             assertThat(getFileRowIdFromDatabase(newFile)).isNotEqualTo(-1);
1187         }
1188     }
1189 
1190     /**
1191      * Asserts cannot rename file.
1192      */
assertCantRenameFile(File oldFile, File newFile)1193     public static void assertCantRenameFile(File oldFile, File newFile) {
1194         final int rowId = getFileRowIdFromDatabase(oldFile);
1195         assertThat(oldFile.renameTo(newFile)).isFalse();
1196         assertThat(oldFile.exists()).isTrue();
1197         assertThat(getFileRowIdFromDatabase(oldFile)).isEqualTo(rowId);
1198     }
1199 
1200     /**
1201      * Assert that app cannot insert files in other app's private directories
1202      *
1203      * @param fileName                    name of the file
1204      * @param throwsExceptionForDataValue Apps like System Gallery for which Data column is not
1205      *                                    respected, will not throw an Exception as the Data value
1206      *                                    is ignored.
1207      * @param otherApp                    Other test app in whose external private directory we will
1208      *                                    attempt to insert
1209      * @param callingPackageName          Calling package name
1210      */
assertCantInsertToOtherPrivateAppDirectories(String fileName, boolean throwsExceptionForDataValue, TestApp otherApp, String callingPackageName)1211     public static void assertCantInsertToOtherPrivateAppDirectories(String fileName,
1212             boolean throwsExceptionForDataValue, TestApp otherApp, String callingPackageName)
1213             throws Exception {
1214         // Create directory in which the device test will try to insert file to
1215         final File otherAppExternalDataDir = new File(getExternalFilesDir().getPath().replace(
1216                 callingPackageName, otherApp.getPackageName()));
1217         final File file = new File(otherAppExternalDataDir, fileName);
1218         String absolutePath = file.getAbsolutePath();
1219 
1220         final ContentValues valuesWithRelativePath = new ContentValues();
1221         final String absoluteDirectoryPath = otherAppExternalDataDir.getAbsolutePath();
1222         valuesWithRelativePath.put(MediaStore.MediaColumns.RELATIVE_PATH,
1223                 absoluteDirectoryPath.substring(absoluteDirectoryPath.indexOf("Android")));
1224         valuesWithRelativePath.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
1225 
1226         try {
1227             assertThat(createFileAs(otherApp, file.getPath())).isTrue();
1228             assertCantInsertDataValue(throwsExceptionForDataValue, absolutePath);
1229             assertCantInsertDataValue(throwsExceptionForDataValue,
1230                     "/sdcard/" + absolutePath.substring(absolutePath.indexOf("Android")));
1231             assertCantInsertDataValue(throwsExceptionForDataValue,
1232                     "/storage/emulated/0/Pictures/../"
1233                             + absolutePath.substring(absolutePath.indexOf("Android")));
1234 
1235             try {
1236                 getContentResolver().insert(MediaStore.Files.getContentUri(VOLUME_EXTERNAL),
1237                         valuesWithRelativePath);
1238                 fail("File insert expected to fail: " + file);
1239             } catch (IllegalArgumentException expected) {
1240             }
1241         } finally {
1242             deleteFileAsNoThrow(otherApp, file.getPath());
1243         }
1244     }
1245 
assertCantInsertDataValue(boolean throwsExceptionForDataValue, String path)1246     private static void assertCantInsertDataValue(boolean throwsExceptionForDataValue,
1247             String path) throws Exception {
1248         if (throwsExceptionForDataValue) {
1249             assertThrowsErrorOnInsertToOtherAppPrivateDirectories(path);
1250         } else {
1251             insertDataWithValue(path);
1252             try (Cursor c = getContentResolver().query(
1253                     MediaStore.Files.getContentUri(VOLUME_EXTERNAL),
1254                     new String[]{MediaStore.MediaColumns.DATA},
1255                     MediaStore.MediaColumns.DATA + "=?", new String[]{path}, null)) {
1256                 assertThat(c.getCount()).isEqualTo(0);
1257             }
1258         }
1259     }
1260 
assertThrowsErrorOnInsertToOtherAppPrivateDirectories(String path)1261     private static void assertThrowsErrorOnInsertToOtherAppPrivateDirectories(String path)
1262             throws Exception {
1263         assertThrows(IllegalArgumentException.class, () -> insertDataWithValue(path));
1264     }
1265 
insertDataWithValue(String path)1266     private static void insertDataWithValue(String path) {
1267         final ContentValues valuesWithData = new ContentValues();
1268         valuesWithData.put(MediaStore.MediaColumns.DATA, path);
1269 
1270         getContentResolver().insert(MediaStore.Files.getContentUri(VOLUME_EXTERNAL),
1271                 valuesWithData);
1272     }
1273 
1274     /**
1275      * Assert that app cannot update files in other app's private directories
1276      *
1277      * @param fileName                    name of the file
1278      * @param throwsExceptionForDataValue Apps like non-legacy System Gallery/MES for which
1279      *                                    Data column is not respected, will not throw an Exception
1280      *                                    as the Data value is ignored.
1281      * @param otherApp                    Other test app in whose external private directory we will
1282      *                                    attempt to insert
1283      * @param callingPackageName          Calling package name
1284      */
assertCantUpdateToOtherPrivateAppDirectories(String fileName, boolean throwsExceptionForDataValue, TestApp otherApp, String callingPackageName)1285     public static void assertCantUpdateToOtherPrivateAppDirectories(String fileName,
1286             boolean throwsExceptionForDataValue, TestApp otherApp, String callingPackageName)
1287             throws Exception {
1288         // Create priv-app file and add to the database that we will try to update
1289         final File otherAppExternalDataDir = new File(getExternalFilesDir().getPath().replace(
1290                 callingPackageName, otherApp.getPackageName()));
1291         final File file = new File(otherAppExternalDataDir, fileName);
1292         try {
1293             assertThat(createFileAs(otherApp, file.getPath())).isTrue();
1294             MediaStore.scanFile(getContentResolver(), file);
1295 
1296             final ContentValues valuesWithData = new ContentValues();
1297             valuesWithData.put(MediaStore.MediaColumns.DATA, file.getAbsolutePath());
1298             try {
1299                 int res = getContentResolver().update(
1300                         MediaStore.Files.getContentUri(VOLUME_EXTERNAL),
1301                         valuesWithData, Bundle.EMPTY);
1302 
1303                 if (throwsExceptionForDataValue) {
1304                     fail("File update expected to fail: " + file);
1305                 } else {
1306                     assertThat(res).isEqualTo(0);
1307                 }
1308             } catch (IllegalArgumentException expected) {
1309             }
1310 
1311             final ContentValues valuesWithRelativePath = new ContentValues();
1312             final String path = file.getAbsolutePath();
1313             valuesWithRelativePath.put(MediaStore.MediaColumns.RELATIVE_PATH,
1314                     path.substring(path.indexOf("Android")));
1315             valuesWithRelativePath.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
1316             try {
1317                 getContentResolver().update(MediaStore.Files.getContentUri(VOLUME_EXTERNAL),
1318                         valuesWithRelativePath, Bundle.EMPTY);
1319                 fail("File update expected to fail: " + file);
1320             } catch (IllegalArgumentException expected) {
1321             }
1322         } finally {
1323             deleteFileAsNoThrow(otherApp, file.getPath());
1324         }
1325     }
1326 
1327     /**
1328      * Asserts can rename directory.
1329      */
assertCanRenameDirectory(File oldDirectory, File newDirectory, @Nullable File[] oldFilesList, @Nullable File[] newFilesList)1330     public static void assertCanRenameDirectory(File oldDirectory, File newDirectory,
1331             @Nullable File[] oldFilesList, @Nullable File[] newFilesList) {
1332         assertThat(oldDirectory.renameTo(newDirectory)).isTrue();
1333         assertThat(oldDirectory.exists()).isFalse();
1334         assertThat(newDirectory.exists()).isTrue();
1335         for (File file : oldFilesList != null ? oldFilesList : new File[0]) {
1336             assertThat(file.exists()).isFalse();
1337             assertThat(getFileRowIdFromDatabase(file)).isEqualTo(-1);
1338         }
1339         for (File file : newFilesList != null ? newFilesList : new File[0]) {
1340             assertThat(file.exists()).isTrue();
1341             assertThat(getFileRowIdFromDatabase(file)).isNotEqualTo(-1);
1342         }
1343     }
1344 
1345     /**
1346      * Asserts cannot rename directory.
1347      */
assertCantRenameDirectory( File oldDirectory, File newDirectory, @Nullable File[] oldFilesList)1348     public static void assertCantRenameDirectory(
1349             File oldDirectory, File newDirectory, @Nullable File[] oldFilesList) {
1350         assertThat(oldDirectory.renameTo(newDirectory)).isFalse();
1351         assertThat(oldDirectory.exists()).isTrue();
1352         for (File file : oldFilesList != null ? oldFilesList : new File[0]) {
1353             assertThat(file.exists()).isTrue();
1354             assertThat(getFileRowIdFromDatabase(file)).isNotEqualTo(-1);
1355         }
1356     }
1357 
assertMountMode(String packageName, int uid, int expectedMountMode)1358     public static void assertMountMode(String packageName, int uid, int expectedMountMode) {
1359         adoptShellPermissionIdentity("android.permission.WRITE_MEDIA_STORAGE");
1360         try {
1361             final StorageManager storageManager = getContext().getSystemService(
1362                     StorageManager.class);
1363             final int actualMountMode = storageManager.getExternalStorageMountMode(uid,
1364                     packageName);
1365             assertWithMessage("mount mode (%s=%s, %s=%s) for package %s and uid %s",
1366                     expectedMountMode, mountModeToString(expectedMountMode),
1367                     actualMountMode, mountModeToString(actualMountMode),
1368                     packageName, uid).that(actualMountMode).isEqualTo(expectedMountMode);
1369         } finally {
1370             dropShellPermissionIdentity();
1371         }
1372     }
1373 
mountModeToString(int mountMode)1374     public static String mountModeToString(int mountMode) {
1375         switch (mountMode) {
1376             case 0:
1377                 return "EXTERNAL_NONE";
1378             case 1:
1379                 return "DEFAULT";
1380             case 2:
1381                 return "INSTALLER";
1382             case 3:
1383                 return "PASS_THROUGH";
1384             case 4:
1385                 return "ANDROID_WRITABLE";
1386             default:
1387                 return "INVALID(" + mountMode + ")";
1388         }
1389     }
1390 
assertCanAccessPrivateAppAndroidDataDir(boolean canAccess, TestApp testApp, String callingPackage, String fileName)1391     public static void assertCanAccessPrivateAppAndroidDataDir(boolean canAccess,
1392             TestApp testApp, String callingPackage, String fileName) throws Exception {
1393         File[] dataDirs = getContext().getExternalFilesDirs(null);
1394         canReadWriteFilesInDirs(dataDirs, canAccess, testApp, callingPackage, fileName);
1395     }
1396 
assertCanAccessPrivateAppAndroidObbDir(boolean canAccess, TestApp testApp, String callingPackage, String fileName)1397     public static void assertCanAccessPrivateAppAndroidObbDir(boolean canAccess,
1398             TestApp testApp, String callingPackage, String fileName) throws Exception {
1399         File[] obbDirs = getContext().getObbDirs();
1400         canReadWriteFilesInDirs(obbDirs, canAccess, testApp, callingPackage, fileName);
1401     }
1402 
canReadWriteFilesInDirs(File[] dirs, boolean canAccess, TestApp testApp, String callingPackage, String fileName)1403     private static void canReadWriteFilesInDirs(File[] dirs, boolean canAccess, TestApp testApp,
1404             String callingPackage, String fileName) throws Exception {
1405         for (File dir : dirs) {
1406             final File otherAppExternalDataDir = new File(dir.getPath().replace(
1407                     callingPackage, testApp.getPackageName()));
1408             final File file = new File(otherAppExternalDataDir, fileName);
1409             try {
1410                 assertThat(file.exists()).isFalse();
1411 
1412                 assertThat(createFileAs(testApp, file.getPath())).isTrue();
1413                 if (canAccess) {
1414                     assertThat(file.canRead()).isTrue();
1415                     assertThat(file.canWrite()).isTrue();
1416                 } else {
1417                     assertThat(file.canRead()).isFalse();
1418                     assertThat(file.canWrite()).isFalse();
1419                 }
1420             } finally {
1421                 deleteFileAsNoThrow(testApp, file.getAbsolutePath());
1422             }
1423         }
1424     }
1425 
1426     /**
1427      * Polls for external storage to be mounted.
1428      */
pollForExternalStorageState()1429     public static void pollForExternalStorageState() throws Exception {
1430         pollForCondition(
1431                 () -> Environment.getExternalStorageState(getExternalStorageDir())
1432                         .equals(Environment.MEDIA_MOUNTED),
1433                 "Timed out while waiting for ExternalStorageState to be MEDIA_MOUNTED");
1434     }
1435 
1436     /**
1437      * Polls until we're granted or denied a given permission.
1438      */
pollForPermission(String perm, boolean granted)1439     public static void pollForPermission(String perm, boolean granted) throws Exception {
1440         pollForCondition(() -> granted == checkPermissionAndAppOp(perm),
1441                 "Timed out while waiting for permission " + perm + " to be "
1442                         + (granted ? "granted" : "revoked"));
1443     }
1444 
1445     /**
1446      * Polls until {@code app} is granted or denied the given permission.
1447      */
pollForPermission(TestApp app, String perm, boolean granted)1448     public static void pollForPermission(TestApp app, String perm, boolean granted)
1449             throws Exception {
1450         pollForPermission(app.getPackageName(), perm, granted);
1451     }
1452 
1453     /**
1454      * Polls until {@code packageName} is granted or denied the given permission.
1455      */
pollForPermission(String packageName, String perm, boolean granted)1456     public static void pollForPermission(String packageName, String perm, boolean granted)
1457             throws Exception {
1458         pollForCondition(
1459                 () -> granted == checkPermission(packageName, perm),
1460                 "Timed out while waiting for permission " + perm + " to be "
1461                         + (granted ? "granted" : "revoked"));
1462     }
1463 
1464     /**
1465      * Returns true iff {@code packageName} is granted a given permission.
1466      */
checkPermission(String packageName, String perm)1467     public static boolean checkPermission(String packageName, String perm) {
1468         try {
1469             int uid = getContext().getPackageManager().getPackageUid(packageName, 0);
1470 
1471             Optional<ActivityManager.RunningAppProcessInfo> process = getAppProcessInfo(
1472                     packageName);
1473             int pid = process.isPresent() ? process.get().pid : -1;
1474             return checkPermissionAndAppOp(perm, packageName, pid, uid);
1475         } catch (PackageManager.NameNotFoundException e) {
1476             return false;
1477         }
1478     }
1479 
1480     /**
1481      * Returns true iff {@code app} is granted a given permission.
1482      */
checkPermission(TestApp app, String perm)1483     public static boolean checkPermission(TestApp app, String perm) {
1484         return checkPermission(app.getPackageName(), perm);
1485     }
1486 
1487     /**
1488      * Asserts the entire content of the file equals exactly {@code expectedContent}.
1489      */
assertFileContent(File file, byte[] expectedContent)1490     public static void assertFileContent(File file, byte[] expectedContent) throws IOException {
1491         try (FileInputStream fis = new FileInputStream(file)) {
1492             assertInputStreamContent(fis, expectedContent);
1493         }
1494     }
1495 
1496     /**
1497      * Asserts the entire content of the file equals exactly {@code expectedContent}.
1498      * <p>Sets {@code fd} to beginning of file first.
1499      */
assertFileContent(FileDescriptor fd, byte[] expectedContent)1500     public static void assertFileContent(FileDescriptor fd, byte[] expectedContent)
1501             throws IOException, ErrnoException {
1502         Os.lseek(fd, 0, OsConstants.SEEK_SET);
1503         try (FileInputStream fis = new FileInputStream(fd)) {
1504             assertInputStreamContent(fis, expectedContent);
1505         }
1506     }
1507 
1508     /**
1509      * Asserts that {@code dir} is a directory and that it doesn't contain any of
1510      * {@code unexpectedContent}
1511      */
assertDirectoryDoesNotContain(@onNull File dir, File... unexpectedContent)1512     public static void assertDirectoryDoesNotContain(@NonNull File dir, File... unexpectedContent) {
1513         assertThat(dir.isDirectory()).isTrue();
1514         assertThat(Arrays.asList(dir.listFiles())).containsNoneIn(unexpectedContent);
1515     }
1516 
1517     /**
1518      * Asserts that {@code dir} is a directory and that it contains all of {@code expectedContent}
1519      */
assertDirectoryContains(@onNull File dir, File... expectedContent)1520     public static void assertDirectoryContains(@NonNull File dir, File... expectedContent) {
1521         assertThat(dir.isDirectory()).isTrue();
1522         assertThat(Arrays.asList(dir.listFiles())).containsAtLeastElementsIn(expectedContent);
1523     }
1524 
getExternalStorageDir()1525     public static File getExternalStorageDir() {
1526         return sExternalStorageDirectory;
1527     }
1528 
setExternalStorageVolume(@onNull String volName)1529     public static void setExternalStorageVolume(@NonNull String volName) {
1530         sStorageVolumeName = volName.toLowerCase(Locale.ROOT);
1531         sExternalStorageDirectory = new File("/storage/" + volName);
1532     }
1533 
1534     /**
1535      * Resets the root directory of external storage to the default.
1536      *
1537      * @see Environment#getExternalStorageDirectory()
1538      */
resetDefaultExternalStorageVolume()1539     public static void resetDefaultExternalStorageVolume() {
1540         sStorageVolumeName = MediaStore.VOLUME_EXTERNAL;
1541         sExternalStorageDirectory = Environment.getExternalStorageDirectory();
1542     }
1543 
1544     /**
1545      * Asserts the default volume used in helper methods is the primary volume.
1546      */
assertDefaultVolumeIsPrimary()1547     public static void assertDefaultVolumeIsPrimary() {
1548         assertVolumeType(true /* isPrimary */);
1549     }
1550 
1551     /**
1552      * Asserts the default volume used in helper methods is a public volume.
1553      */
assertDefaultVolumeIsPublic()1554     public static void assertDefaultVolumeIsPublic() {
1555         assertVolumeType(false /* isPrimary */);
1556     }
1557 
1558     /**
1559      * Creates and returns the Android data sub-directory belonging to the calling package.
1560      */
getExternalFilesDir()1561     public static File getExternalFilesDir() {
1562         final String packageName = getContext().getPackageName();
1563         final File res = new File(getAndroidDataDir(), packageName + "/files");
1564         if (!res.equals(getContext().getExternalFilesDir(null))) {
1565             res.mkdirs();
1566         }
1567         return res;
1568     }
1569 
1570     /**
1571      * Creates and returns the Android obb sub-directory belonging to the calling package.
1572      */
getExternalObbDir()1573     public static File getExternalObbDir() {
1574         final String packageName = getContext().getPackageName();
1575         final File res = new File(getAndroidObbDir(), packageName);
1576         if (!res.equals(getContext().getObbDirs()[0])) {
1577             res.mkdirs();
1578         }
1579         return res;
1580     }
1581 
1582     /**
1583      * Creates and returns the Android media sub-directory belonging to the calling package.
1584      */
getExternalMediaDir()1585     public static File getExternalMediaDir() {
1586         final String packageName = getContext().getPackageName();
1587         final File res = new File(getAndroidMediaDir(), packageName);
1588         if (!res.equals(getContext().getExternalMediaDirs()[0])) {
1589             res.mkdirs();
1590         }
1591         return res;
1592     }
1593 
getAlarmsDir()1594     public static File getAlarmsDir() {
1595         return new File(getExternalStorageDir(),
1596                 Environment.DIRECTORY_ALARMS);
1597     }
1598 
getAndroidDir()1599     public static File getAndroidDir() {
1600         return new File(getExternalStorageDir(),
1601                 "Android");
1602     }
1603 
getAudiobooksDir()1604     public static File getAudiobooksDir() {
1605         return new File(getExternalStorageDir(),
1606                 Environment.DIRECTORY_AUDIOBOOKS);
1607     }
1608 
getDcimDir()1609     public static File getDcimDir() {
1610         return new File(getExternalStorageDir(), Environment.DIRECTORY_DCIM);
1611     }
1612 
getDocumentsDir()1613     public static File getDocumentsDir() {
1614         return new File(getExternalStorageDir(),
1615                 Environment.DIRECTORY_DOCUMENTS);
1616     }
1617 
getDownloadDir()1618     public static File getDownloadDir() {
1619         return new File(getExternalStorageDir(),
1620                 Environment.DIRECTORY_DOWNLOADS);
1621     }
1622 
getMusicDir()1623     public static File getMusicDir() {
1624         return new File(getExternalStorageDir(),
1625                 Environment.DIRECTORY_MUSIC);
1626     }
1627 
getMoviesDir()1628     public static File getMoviesDir() {
1629         return new File(getExternalStorageDir(),
1630                 Environment.DIRECTORY_MOVIES);
1631     }
1632 
getNotificationsDir()1633     public static File getNotificationsDir() {
1634         return new File(getExternalStorageDir(),
1635                 Environment.DIRECTORY_NOTIFICATIONS);
1636     }
1637 
getPicturesDir()1638     public static File getPicturesDir() {
1639         return new File(getExternalStorageDir(),
1640                 Environment.DIRECTORY_PICTURES);
1641     }
1642 
getPodcastsDir()1643     public static File getPodcastsDir() {
1644         return new File(getExternalStorageDir(),
1645                 Environment.DIRECTORY_PODCASTS);
1646     }
1647 
getRecordingsDir()1648     public static File getRecordingsDir() {
1649         return new File(getExternalStorageDir(),
1650                 Environment.DIRECTORY_RECORDINGS);
1651     }
1652 
getRingtonesDir()1653     public static File getRingtonesDir() {
1654         return new File(getExternalStorageDir(),
1655                 Environment.DIRECTORY_RINGTONES);
1656     }
1657 
getAndroidDataDir()1658     public static File getAndroidDataDir() {
1659         return new File(getAndroidDir(), "data");
1660     }
1661 
getAndroidObbDir()1662     public static File getAndroidObbDir() {
1663         return new File(getAndroidDir(), "obb");
1664     }
1665 
getAndroidMediaDir()1666     public static File getAndroidMediaDir() {
1667         return new File(getAndroidDir(), "media");
1668     }
1669 
getDefaultTopLevelDirs()1670     public static File[] getDefaultTopLevelDirs() {
1671         if (BuildCompat.isAtLeastS()) {
1672             return new File[]{getAlarmsDir(), getAndroidDir(), getAudiobooksDir(), getDcimDir(),
1673                     getDocumentsDir(), getDownloadDir(), getMusicDir(), getMoviesDir(),
1674                     getNotificationsDir(), getPicturesDir(), getPodcastsDir(), getRecordingsDir(),
1675                     getRingtonesDir()};
1676         }
1677         return new File[]{getAlarmsDir(), getAndroidDir(), getAudiobooksDir(), getDcimDir(),
1678                 getDocumentsDir(), getDownloadDir(), getMusicDir(), getMoviesDir(),
1679                 getNotificationsDir(), getPicturesDir(), getPodcastsDir(),
1680                 getRingtonesDir()};
1681     }
1682 
assertInputStreamContent(InputStream in, byte[] expectedContent)1683     private static void assertInputStreamContent(InputStream in, byte[] expectedContent)
1684             throws IOException {
1685         assertThat(ByteStreams.toByteArray(in)).isEqualTo(expectedContent);
1686     }
1687 
1688     /**
1689      * Checks if the given {@code permission} is granted and corresponding AppOp is MODE_ALLOWED.
1690      */
checkPermissionAndAppOp(String permission)1691     private static boolean checkPermissionAndAppOp(String permission) {
1692         final int pid = Os.getpid();
1693         final int uid = Os.getuid();
1694         final String packageName = getContext().getPackageName();
1695         return checkPermissionAndAppOp(permission, packageName, pid, uid);
1696     }
1697 
1698     /**
1699      * Checks if the given {@code permission} is granted and corresponding AppOp is MODE_ALLOWED.
1700      */
checkPermissionAndAppOp(String permission, String packageName, int pid, int uid)1701     private static boolean checkPermissionAndAppOp(String permission, String packageName, int pid,
1702             int uid) {
1703         final Context context = getContext();
1704         if (context.checkPermission(permission, pid, uid) != PackageManager.PERMISSION_GRANTED) {
1705             return false;
1706         }
1707 
1708         final String op = AppOpsManager.permissionToOp(permission);
1709         // No AppOp associated with the given permission, skip AppOp check.
1710         if (op == null) {
1711             return true;
1712         }
1713 
1714         final AppOpsManager appOps = context.getSystemService(AppOpsManager.class);
1715         try {
1716             appOps.checkPackage(uid, packageName);
1717         } catch (SecurityException e) {
1718             return false;
1719         }
1720 
1721         return appOps.unsafeCheckOpNoThrow(op, uid, packageName) == AppOpsManager.MODE_ALLOWED;
1722     }
1723 
1724     /**
1725      * <p>This method drops shell permission identity.
1726      */
forceStopApp(String packageName)1727     public static void forceStopApp(String packageName) throws Exception {
1728         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
1729         try {
1730             uiAutomation.adoptShellPermissionIdentity(Manifest.permission.FORCE_STOP_PACKAGES);
1731 
1732             getContext().getSystemService(ActivityManager.class).forceStopPackage(packageName);
1733             pollForCondition(() -> {
1734                 return !isProcessRunning(packageName);
1735             }, "Timed out while waiting for " + packageName + " to be stopped");
1736         } finally {
1737             uiAutomation.dropShellPermissionIdentity();
1738         }
1739     }
1740 
launchTestApp(TestApp testApp, String actionName, BroadcastReceiver broadcastReceiver, CountDownLatch latch, Intent intent)1741     private static void launchTestApp(TestApp testApp, String actionName,
1742             BroadcastReceiver broadcastReceiver, CountDownLatch latch, Intent intent)
1743             throws InterruptedException, TimeoutException {
1744 
1745         // Register broadcast receiver
1746         final IntentFilter intentFilter = new IntentFilter();
1747         intentFilter.addAction(actionName);
1748         intentFilter.addCategory(Intent.CATEGORY_DEFAULT);
1749         getContext().registerReceiver(broadcastReceiver, intentFilter,
1750                 Context.RECEIVER_EXPORTED_UNAUDITED);
1751 
1752         // Launch the test app.
1753         intent.setPackage(testApp.getPackageName());
1754         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1755         intent.putExtra(QUERY_TYPE, actionName);
1756         intent.putExtra(INTENT_EXTRA_CALLING_PKG, getContext().getPackageName());
1757         intent.addCategory(Intent.CATEGORY_LAUNCHER);
1758         getContext().startActivity(intent);
1759         if (!latch.await(POLLING_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
1760             final String errorMessage = "Timed out while waiting to receive " + actionName
1761                     + " intent from " + testApp.getPackageName();
1762             throw new TimeoutException(errorMessage);
1763         }
1764         getContext().unregisterReceiver(broadcastReceiver);
1765     }
1766 
1767     /**
1768      * Sends intent to {@code testApp} for actions on {@code dirPath}
1769      *
1770      * <p>This method drops shell permission identity.
1771      */
sendIntentToTestApp(TestApp testApp, String dirPath, String actionName, IBinder fileDescriptorBinder, BroadcastReceiver broadcastReceiver, CountDownLatch latch)1772     private static void sendIntentToTestApp(TestApp testApp, String dirPath, String actionName,
1773             IBinder fileDescriptorBinder, BroadcastReceiver broadcastReceiver, CountDownLatch latch)
1774             throws Exception {
1775         if (sShouldForceStopTestApp) {
1776             final String packageName = testApp.getPackageName();
1777             forceStopApp(packageName);
1778         }
1779 
1780         // Launch the test app.
1781         final Intent intent = new Intent(Intent.ACTION_MAIN);
1782         intent.putExtra(INTENT_EXTRA_PATH, dirPath);
1783         if (fileDescriptorBinder != null) {
1784             final Bundle bundle = new Bundle();
1785             bundle.putBinder(INTENT_EXTRA_CONTENT, fileDescriptorBinder);
1786             intent.putExtra(INTENT_EXTRA_CONTENT, bundle);
1787         }
1788         launchTestApp(testApp, actionName, broadcastReceiver, latch, intent);
1789     }
1790 
1791     /**
1792      * Sends intent to {@code testApp} for actions on {@code uri}
1793      *
1794      * <p>This method drops shell permission identity.
1795      */
sendIntentToTestApp(TestApp testApp, Uri uri, String actionName, BroadcastReceiver broadcastReceiver, CountDownLatch latch, Bundle args)1796     private static void sendIntentToTestApp(TestApp testApp, Uri uri, String actionName,
1797             BroadcastReceiver broadcastReceiver, CountDownLatch latch,
1798             Bundle args) throws Exception {
1799         if (sShouldForceStopTestApp) {
1800             final String packageName = testApp.getPackageName();
1801             forceStopApp(packageName);
1802         }
1803 
1804         final Intent intent = new Intent(Intent.ACTION_MAIN);
1805         intent.putExtra(INTENT_EXTRA_URI, uri);
1806         intent.putExtra(INTENT_EXTRA_ARGS, args);
1807         launchTestApp(testApp, actionName, broadcastReceiver, latch, intent);
1808     }
1809 
1810     /**
1811      * Gets images/video metadata from a test app.
1812      *
1813      * <p>This method drops shell permission identity.
1814      */
getMetadataFromTestApp( TestApp testApp, String dirPath, String actionName)1815     private static HashMap<String, String> getMetadataFromTestApp(
1816             TestApp testApp, String dirPath, String actionName) throws Exception {
1817         Bundle bundle = getFromTestApp(testApp, dirPath, actionName);
1818         return (HashMap<String, String>) bundle.get(actionName);
1819     }
1820 
1821     /**
1822      * <p>This method drops shell permission identity.
1823      */
getContentsFromTestApp( TestApp testApp, String dirPath, String actionName)1824     private static ArrayList<String> getContentsFromTestApp(
1825             TestApp testApp, String dirPath, String actionName) throws Exception {
1826         Bundle bundle = getFromTestApp(testApp, dirPath, actionName);
1827         return bundle.getStringArrayList(actionName);
1828     }
1829 
1830     /**
1831      * <p>This method drops shell permission identity.
1832      */
getResultFromTestApp(TestApp testApp, String dirPath, String actionName)1833     private static boolean getResultFromTestApp(TestApp testApp, String dirPath, String actionName)
1834             throws Exception {
1835         Bundle bundle = getFromTestApp(testApp, dirPath, actionName);
1836         return bundle.getBoolean(actionName, false);
1837     }
1838 
1839     /**
1840      * <p>This method drops shell permission identity.
1841      */
getResultFromTestApp(TestApp testApp, String dirPath, String actionName, IBinder fileDescriptorBinder)1842     private static boolean getResultFromTestApp(TestApp testApp, String dirPath, String actionName,
1843             IBinder fileDescriptorBinder)
1844             throws Exception {
1845         Bundle bundle = getFromTestApp(testApp, dirPath, actionName, fileDescriptorBinder);
1846         return bundle.getBoolean(actionName, false);
1847     }
1848 
1849 
getPfdFromTestApp(TestApp testApp, File dirPath, String actionName, String mode)1850     private static ParcelFileDescriptor getPfdFromTestApp(TestApp testApp, File dirPath,
1851             String actionName, String mode) throws Exception {
1852         Bundle bundle = getFromTestApp(testApp, dirPath.getPath(), actionName);
1853         return getContentResolver().openFileDescriptor(bundle.getParcelable(actionName), mode);
1854     }
1855 
1856     /**
1857      * <p>This method drops shell permission identity.
1858      */
getFromTestApp(TestApp testApp, String dirPath, String actionName)1859     private static Bundle getFromTestApp(TestApp testApp, String dirPath, String actionName)
1860             throws Exception {
1861         return getFromTestApp(testApp, dirPath, actionName, null);
1862     }
1863 
1864     /**
1865      * <p>This method drops shell permission identity.
1866      */
getFromTestApp(TestApp testApp, String dirPath, String actionName, @Nullable IBinder fileDescriptorBinder)1867     private static Bundle getFromTestApp(TestApp testApp, String dirPath, String actionName,
1868             @Nullable IBinder fileDescriptorBinder)
1869             throws Exception {
1870         final CountDownLatch latch = new CountDownLatch(1);
1871         final Bundle[] bundle = new Bundle[1];
1872         final Exception[] exception = new Exception[1];
1873         exception[0] = null;
1874         BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
1875             @Override
1876             public void onReceive(Context context, Intent intent) {
1877                 if (intent.hasExtra(INTENT_EXCEPTION)) {
1878                     exception[0] = (Exception) (intent.getSerializableExtra(INTENT_EXCEPTION));
1879                 } else {
1880                     bundle[0] = intent.getExtras();
1881                 }
1882                 latch.countDown();
1883             }
1884         };
1885 
1886         sendIntentToTestApp(testApp, dirPath, actionName, fileDescriptorBinder, broadcastReceiver,
1887                 latch);
1888         if (exception[0] != null) {
1889             throw exception[0];
1890         }
1891         return bundle[0];
1892     }
1893 
1894     /**
1895      * <p>This method drops shell permission identity.
1896      */
getFromTestApp(TestApp testApp, Uri uri, String actionName)1897     private static Bundle getFromTestApp(TestApp testApp, Uri uri, String actionName)
1898             throws Exception {
1899         return getFromTestApp(testApp, uri, actionName, null);
1900     }
1901 
1902     /**
1903      * <p>This method drops shell permission identity.
1904      */
getFromTestApp(TestApp testApp, Uri uri, String actionName, Bundle args)1905     private static Bundle getFromTestApp(TestApp testApp, Uri uri, String actionName, Bundle args)
1906             throws Exception {
1907         final CountDownLatch latch = new CountDownLatch(1);
1908         final Bundle[] bundle = new Bundle[1];
1909         final Exception[] exception = new Exception[1];
1910         exception[0] = null;
1911         BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
1912             @Override
1913             public void onReceive(Context context, Intent intent) {
1914                 if (intent.hasExtra(INTENT_EXCEPTION)) {
1915                     exception[0] = (Exception) (intent.getSerializableExtra(INTENT_EXCEPTION));
1916                 } else {
1917                     bundle[0] = intent.getExtras();
1918                 }
1919                 latch.countDown();
1920             }
1921         };
1922 
1923         sendIntentToTestApp(testApp, uri, actionName, broadcastReceiver, latch, args);
1924         if (exception[0] != null) {
1925             throw exception[0];
1926         }
1927         return bundle[0];
1928     }
1929 
1930     /**
1931      * Sets {@code mode} for the given {@code ops} and the given {@code uid}.
1932      *
1933      * <p>This method drops shell permission identity.
1934      */
setAppOpsModeForUid(int uid, int mode, @NonNull String... ops)1935     public static void setAppOpsModeForUid(int uid, int mode, @NonNull String... ops) {
1936         adoptShellPermissionIdentity(null);
1937         try {
1938             for (String op : ops) {
1939                 getContext().getSystemService(AppOpsManager.class).setUidMode(op, uid, mode);
1940             }
1941         } finally {
1942             dropShellPermissionIdentity();
1943         }
1944     }
1945 
1946     /**
1947      * Queries {@link ContentResolver} for a file IS_PENDING=0 and returns a {@link Cursor} with the
1948      * given columns.
1949      */
1950     @NonNull
queryFileExcludingPending(@onNull File file, String... projection)1951     public static Cursor queryFileExcludingPending(@NonNull File file, String... projection) {
1952         return queryFile(getContentResolver(), MediaStore.Files.getContentUri(sStorageVolumeName),
1953                 file, /*includePending*/ false, projection);
1954     }
1955 
1956     @NonNull
queryFile(ContentResolver cr, @NonNull File file, String... projection)1957     public static Cursor queryFile(ContentResolver cr, @NonNull File file, String... projection) {
1958         return queryFile(cr, MediaStore.Files.getContentUri(sStorageVolumeName),
1959                 file, /*includePending*/ true, projection);
1960     }
1961 
1962     @NonNull
queryFile(@onNull File file, String... projection)1963     public static Cursor queryFile(@NonNull File file, String... projection) {
1964         return queryFile(getContentResolver(), MediaStore.Files.getContentUri(sStorageVolumeName),
1965                 file, /*includePending*/ true, projection);
1966     }
1967 
1968     @NonNull
queryFile(ContentResolver cr, @NonNull Uri uri, @NonNull File file, boolean includePending, String... projection)1969     private static Cursor queryFile(ContentResolver cr, @NonNull Uri uri, @NonNull File file,
1970             boolean includePending, String... projection) {
1971         Bundle queryArgs = new Bundle();
1972         queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SELECTION,
1973                 MediaStore.MediaColumns.DATA + " = ?");
1974         queryArgs.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
1975                 new String[]{file.getAbsolutePath()});
1976         queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE);
1977 
1978         if (includePending) {
1979             queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_INCLUDE);
1980         } else {
1981             queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_EXCLUDE);
1982         }
1983 
1984         final Cursor c = cr.query(uri, projection, queryArgs, null);
1985         assertThat(c).isNotNull();
1986         return c;
1987     }
1988 
isObbDirUnmounted()1989     private static boolean isObbDirUnmounted() {
1990         List<String> mounts = new ArrayList<>();
1991         try {
1992             for (String line : executeShellCommand("cat /proc/mounts").split("\n")) {
1993                 String[] split = line.split(" ");
1994                 // Only check obb dirs with tmpfs, as if it's mounted for app data
1995                 // isolation, it will be tmpfs only.
1996                 if (split[0].equals("tmpfs") && split[1].startsWith("/storage/")
1997                         && split[1].endsWith("/obb")) {
1998                     return false;
1999                 }
2000             }
2001         } catch (IOException e) {
2002             Log.e(TAG, "Failed to execute shell command", e);
2003         }
2004         return true;
2005     }
2006 
isVolumeMounted(String type)2007     private static boolean isVolumeMounted(String type) {
2008         try {
2009             final String volume = executeShellCommand("sm list-volumes " + type).trim();
2010             return volume != null && volume.contains(" mounted");
2011         } catch (Exception e) {
2012             return false;
2013         }
2014     }
2015 
isPublicVolumeMounted()2016     private static boolean isPublicVolumeMounted() {
2017         return isVolumeMounted("public");
2018     }
2019 
isEmulatedVolumeMounted()2020     private static boolean isEmulatedVolumeMounted() {
2021         return isVolumeMounted("emulated");
2022     }
2023 
isFuseReady()2024     private static boolean isFuseReady() {
2025         for (String volumeName : MediaStore.getExternalVolumeNames(getContext())) {
2026             final Uri uri = MediaStore.Files.getContentUri(volumeName);
2027             try (Cursor c = getContentResolver().query(uri, null, null, null)) {
2028                 assertThat(c).isNotNull();
2029             } catch (IllegalArgumentException e) {
2030                 return false;
2031             }
2032         }
2033         return true;
2034     }
2035 
2036     /**
2037      * Prepare or create a public volume for testing
2038      */
preparePublicVolume()2039     public static void preparePublicVolume() throws Exception {
2040         if (getCurrentPublicVolumeName() == null) {
2041             createNewPublicVolume();
2042             return;
2043         }
2044 
2045         if (!Boolean.parseBoolean(executeShellCommand("sm has-adoptable").trim())) {
2046             unmountAppDirs();
2047             // ensure the volume is visible
2048             executeShellCommand("sm set-force-adoptable on");
2049             Thread.sleep(2000);
2050             pollForCondition(TestUtils::isPublicVolumeMounted,
2051                     "Timed out while waiting for public volume");
2052             pollForCondition(TestUtils::isEmulatedVolumeMounted,
2053                     "Timed out while waiting for emulated volume");
2054             pollForCondition(TestUtils::isFuseReady,
2055                     "Timed out while waiting for fuse");
2056         }
2057     }
2058 
isAdoptableStorageSupported()2059     public static boolean isAdoptableStorageSupported() throws Exception {
2060         return hasAdoptableStorageFeature() || hasAdoptableStorageFstab();
2061     }
2062 
hasAdoptableStorageFstab()2063     private static boolean hasAdoptableStorageFstab() throws Exception {
2064         return Boolean.parseBoolean(executeShellCommand("sm has-adoptable").trim());
2065     }
2066 
hasAdoptableStorageFeature()2067     private static boolean hasAdoptableStorageFeature() throws Exception {
2068         return getContext().getPackageManager().hasSystemFeature(
2069                 PackageManager.FEATURE_ADOPTABLE_STORAGE);
2070     }
2071 
2072     /**
2073      * Unmount app's obb and data dirs.
2074      */
unmountAppDirs()2075     public static void unmountAppDirs() throws Exception {
2076         if (TestUtils.isObbDirUnmounted()) {
2077             return;
2078         }
2079         executeShellCommand("sm unmount-app-data-dirs " + getContext().getPackageName() + " "
2080                 + android.os.Process.myPid() + " " + android.os.UserHandle.myUserId());
2081         pollForCondition(TestUtils::isObbDirUnmounted,
2082                 "Timed out while waiting for unmounting obb dir");
2083     }
2084 
2085     /**
2086      * Creates a new virtual public volume and returns the volume's name.
2087      */
createNewPublicVolume()2088     public static void createNewPublicVolume() throws Exception {
2089         // Unmount data and obb dirs for test app first so test app won't be killed during
2090         // volume unmount.
2091         unmountAppDirs();
2092         executeShellCommand("sm set-force-adoptable on");
2093         executeShellCommand("sm set-virtual-disk true");
2094         Thread.sleep(2000);
2095         pollForCondition(TestUtils::partitionDisk, "Timed out while waiting for disk partitioning");
2096     }
2097 
partitionDisk()2098     private static boolean partitionDisk() {
2099         try {
2100             final String listDisks = executeShellCommand("sm list-disks").trim();
2101             if (TextUtils.isEmpty(listDisks)) {
2102                 return false;
2103             }
2104             executeShellCommand("sm partition " + listDisks + " public");
2105             return true;
2106         } catch (Exception e) {
2107             return false;
2108         }
2109     }
2110 
2111     /**
2112      * Gets the name of the public volume, waiting for a bit for it to be available.
2113      */
getPublicVolumeName()2114     public static String getPublicVolumeName() throws Exception {
2115         final String[] volName = new String[1];
2116         pollForCondition(() -> {
2117             volName[0] = getCurrentPublicVolumeName();
2118             return volName[0] != null;
2119         }, "Timed out while waiting for public volume to be ready");
2120 
2121         return volName[0];
2122     }
2123 
2124     /**
2125      * @return the currently mounted public volume, if any.
2126      */
getCurrentPublicVolumeName()2127     public static String getCurrentPublicVolumeName() {
2128         final String[] allVolumeDetails;
2129         try {
2130             allVolumeDetails = executeShellCommand("sm list-volumes")
2131                     .trim().split("\n");
2132         } catch (Exception e) {
2133             Log.e(TAG, "Failed to execute shell command", e);
2134             return null;
2135         }
2136         for (String volDetails : allVolumeDetails) {
2137             if (volDetails.startsWith("public")) {
2138                 final String[] publicVolumeDetails = volDetails.trim().split(" ");
2139                 String res = publicVolumeDetails[publicVolumeDetails.length - 1];
2140                 if ("null".equals(res)) {
2141                     continue;
2142                 }
2143                 return res;
2144             }
2145         }
2146         return null;
2147     }
2148 
2149     /**
2150      * Returns the content URI of the volume on which the test is running.
2151      */
getTestVolumeFileUri()2152     public static Uri getTestVolumeFileUri() {
2153         return MediaStore.Files.getContentUri(sStorageVolumeName);
2154     }
2155 
pollForCondition(Supplier<Boolean> condition, String errorMessage)2156     private static void pollForCondition(Supplier<Boolean> condition, String errorMessage)
2157             throws Exception {
2158         for (int i = 0; i < POLLING_TIMEOUT_MILLIS / POLLING_SLEEP_MILLIS; i++) {
2159             if (condition.get()) {
2160                 return;
2161             }
2162             Thread.sleep(POLLING_SLEEP_MILLIS);
2163         }
2164         throw new TimeoutException(errorMessage);
2165     }
2166 
2167     /**
2168      * Polls for all files access to be allowed.
2169      */
pollForManageExternalStorageAllowed()2170     public static void pollForManageExternalStorageAllowed() throws Exception {
2171         pollForCondition(
2172                 () -> Environment.isExternalStorageManager(),
2173                 "Timed out while waiting for MANAGE_EXTERNAL_STORAGE");
2174     }
2175 
assertVolumeType(boolean isPrimary)2176     private static void assertVolumeType(boolean isPrimary) {
2177         String[] parts = getExternalFilesDir().getAbsolutePath().split("/");
2178         assertThat(parts.length).isAtLeast(3);
2179         assertThat(parts[1]).isEqualTo("storage");
2180         if (isPrimary) {
2181             assertThat(parts[2]).isEqualTo("emulated");
2182         } else {
2183             assertThat(parts[2]).isNotEqualTo("emulated");
2184         }
2185     }
2186 
isProcessRunning(String packageName)2187     private static boolean isProcessRunning(String packageName) {
2188         return getAppProcessInfo(packageName).isPresent();
2189     }
2190 
getAppProcessInfo( String packageName)2191     private static Optional<ActivityManager.RunningAppProcessInfo> getAppProcessInfo(
2192             String packageName) {
2193         return getContext().getSystemService(ActivityManager.class)
2194                 .getRunningAppProcesses()
2195                 .stream()
2196                 .filter(p -> packageName.equals(p.processName))
2197                 .findFirst();
2198     }
2199 
trashFileAndAssert(Uri uri)2200     public static void trashFileAndAssert(Uri uri) {
2201         final ContentValues values = new ContentValues();
2202         values.put(MediaStore.MediaColumns.IS_TRASHED, 1);
2203         assertWithMessage("Result of ContentResolver#update for " + uri + " with values to trash "
2204                 + "file " + values)
2205                 .that(getContentResolver().update(uri, values, Bundle.EMPTY)).isEqualTo(1);
2206     }
2207 
untrashFileAndAssert(Uri uri)2208     public static void untrashFileAndAssert(Uri uri) {
2209         final ContentValues values = new ContentValues();
2210         values.put(MediaStore.MediaColumns.IS_TRASHED, 0);
2211         assertWithMessage("Result of ContentResolver#update for " + uri + " with values to untrash "
2212                 + "file " + values)
2213                 .that(getContentResolver().update(uri, values, Bundle.EMPTY)).isEqualTo(1);
2214     }
2215 
waitForMountedAndIdleState(ContentResolver resolver)2216     public static void waitForMountedAndIdleState(ContentResolver resolver) throws Exception {
2217         // We purposefully perform these operations twice in this specific
2218         // order, since clearing the data on a package can asynchronously
2219         // perform a vold reset, which can make us think storage is ready and
2220         // mounted when it's moments away from being torn down.
2221         pollForExternalStorageMountedState();
2222         MediaStore.waitForIdle(resolver);
2223         pollForExternalStorageMountedState();
2224         MediaStore.waitForIdle(resolver);
2225     }
2226 
pollForExternalStorageMountedState()2227     private static void pollForExternalStorageMountedState() throws Exception {
2228         final File target = Environment.getExternalStorageDirectory();
2229         pollForCondition(() -> isExternalStorageDirectoryMounted(target),
2230                 "Timed out while waiting for ExternalStorageState to be MEDIA_MOUNTED");
2231     }
2232 
isExternalStorageDirectoryMounted(File target)2233     private static boolean isExternalStorageDirectoryMounted(File target) {
2234         boolean isMounted = Environment.MEDIA_MOUNTED.equals(
2235                 Environment.getExternalStorageState(target));
2236         if (isMounted) {
2237             try {
2238                 return Os.statvfs(target.getAbsolutePath()).f_blocks > 0;
2239             } catch (Exception e) {
2240                 // Waiting for external storage to be mounted
2241             }
2242         }
2243         return false;
2244     }
2245 }
2246