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.scopedstorage.cts.lib.RedactionTestHelper.EXIF_METADATA_QUERY;
20 
21 import static androidx.test.InstrumentationRegistry.getContext;
22 
23 import static com.google.common.truth.Truth.assertThat;
24 import static com.google.common.truth.Truth.assertWithMessage;
25 
26 import static junit.framework.Assert.assertEquals;
27 import static junit.framework.TestCase.assertNotNull;
28 
29 import static org.junit.Assert.assertNotEquals;
30 import static org.junit.Assert.fail;
31 
32 import android.Manifest;
33 import android.app.ActivityManager;
34 import android.app.AppOpsManager;
35 import android.app.UiAutomation;
36 import android.content.BroadcastReceiver;
37 import android.content.ContentResolver;
38 import android.content.ContentUris;
39 import android.content.ContentValues;
40 import android.content.Context;
41 import android.content.Intent;
42 import android.content.IntentFilter;
43 import android.content.pm.PackageManager;
44 import android.database.Cursor;
45 import android.net.Uri;
46 import android.os.Bundle;
47 import android.os.Environment;
48 import android.os.ParcelFileDescriptor;
49 import android.os.storage.StorageManager;
50 import android.provider.MediaStore;
51 import android.system.ErrnoException;
52 import android.system.Os;
53 import android.system.OsConstants;
54 import android.text.TextUtils;
55 import android.util.Log;
56 
57 import androidx.annotation.NonNull;
58 import androidx.annotation.Nullable;
59 import androidx.core.os.BuildCompat;
60 import androidx.test.InstrumentationRegistry;
61 
62 import com.android.cts.install.lib.Install;
63 import com.android.cts.install.lib.InstallUtils;
64 import com.android.cts.install.lib.TestApp;
65 import com.android.cts.install.lib.Uninstall;
66 
67 import com.google.common.io.ByteStreams;
68 
69 import java.io.File;
70 import java.io.FileDescriptor;
71 import java.io.FileInputStream;
72 import java.io.IOException;
73 import java.io.InputStream;
74 import java.io.InterruptedIOException;
75 import java.util.ArrayList;
76 import java.util.Arrays;
77 import java.util.HashMap;
78 import java.util.List;
79 import java.util.Locale;
80 import java.util.Optional;
81 import java.util.concurrent.CountDownLatch;
82 import java.util.concurrent.TimeUnit;
83 import java.util.concurrent.TimeoutException;
84 import java.util.function.Supplier;
85 
86 /**
87  * General helper functions for ScopedStorageTest tests.
88  */
89 public class TestUtils {
90     static final String TAG = "ScopedStorageTest";
91 
92     public static final String QUERY_TYPE = "android.scopedstorage.cts.queryType";
93     public static final String INTENT_EXTRA_PATH = "android.scopedstorage.cts.path";
94     public static final String INTENT_EXTRA_URI = "android.scopedstorage.cts.uri";
95     public static final String INTENT_EXTRA_CALLING_PKG = "android.scopedstorage.cts.calling_pkg";
96     public static final String INTENT_EXCEPTION = "android.scopedstorage.cts.exception";
97     public static final String CREATE_FILE_QUERY = "android.scopedstorage.cts.createfile";
98     public static final String CREATE_IMAGE_ENTRY_QUERY =
99             "android.scopedstorage.cts.createimageentry";
100     public static final String DELETE_FILE_QUERY = "android.scopedstorage.cts.deletefile";
101     public static final String CAN_OPEN_FILE_FOR_READ_QUERY =
102             "android.scopedstorage.cts.can_openfile_read";
103     public static final String CAN_OPEN_FILE_FOR_WRITE_QUERY =
104             "android.scopedstorage.cts.can_openfile_write";
105     public static final String IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_READ =
106             "android.scopedstorage.cts.is_uri_redacted_via_file_descriptor_for_read";
107     public static final String IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_WRITE =
108             "android.scopedstorage.cts.is_uri_redacted_via_file_descriptor_for_write";
109     public static final String IS_URI_REDACTED_VIA_FILEPATH =
110             "android.scopedstorage.cts.is_uri_redacted_via_filepath";
111     public static final String QUERY_URI = "android.scopedstorage.cts.query_uri";
112     public static final String OPEN_FILE_FOR_READ_QUERY =
113             "android.scopedstorage.cts.openfile_read";
114     public static final String OPEN_FILE_FOR_WRITE_QUERY =
115             "android.scopedstorage.cts.openfile_write";
116     public static final String CAN_READ_WRITE_QUERY =
117             "android.scopedstorage.cts.can_read_and_write";
118     public static final String READDIR_QUERY = "android.scopedstorage.cts.readdir";
119     public static final String SETATTR_QUERY = "android.scopedstorage.cts.setattr";
120     public static final String CHECK_DATABASE_ROW_EXISTS_QUERY =
121             "android.scopedstorage.cts.check_database_row_exists";
122     public static final String RENAME_FILE_QUERY = "android.scopedstorage.cts.renamefile";
123 
124     public static final String STR_DATA1 = "Just some random text";
125     public static final String STR_DATA2 = "More arbitrary stuff";
126 
127     public static final byte[] BYTES_DATA1 = STR_DATA1.getBytes();
128     public static final byte[] BYTES_DATA2 = STR_DATA2.getBytes();
129 
130     public static final String RENAME_FILE_PARAMS_SEPARATOR = ";";
131 
132     // Root of external storage
133     private static File sExternalStorageDirectory = Environment.getExternalStorageDirectory();
134     private static String sStorageVolumeName = MediaStore.VOLUME_EXTERNAL;
135 
136     /**
137      * Set this to {@code false} if the test is verifying uri grants on testApp. Force stopping the
138      * app will kill the app and it will lose uri grants.
139      */
140     private static boolean sShouldForceStopTestApp = true;
141 
142     private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(20);
143     private static final long POLLING_SLEEP_MILLIS = 100;
144 
145     /**
146      * Creates the top level default directories.
147      *
148      * <p>Those are usually created by MediaProvider, but some naughty tests might delete them
149      * and not restore them afterwards, so we make sure we create them before we make any
150      * assumptions about their existence.
151      */
setupDefaultDirectories()152     public static void setupDefaultDirectories() {
153         for (File dir : getDefaultTopLevelDirs()) {
154             dir.mkdirs();
155             assertWithMessage("Could not setup default dir [%s]", dir.toString())
156                     .that(dir.exists())
157                     .isTrue();
158         }
159     }
160 
161     /**
162      * Grants {@link Manifest.permission#GRANT_RUNTIME_PERMISSIONS} to the given package.
163      */
grantPermission(String packageName, String permission)164     public static void grantPermission(String packageName, String permission) {
165         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
166         uiAutomation.adoptShellPermissionIdentity("android.permission.GRANT_RUNTIME_PERMISSIONS");
167         try {
168             uiAutomation.grantRuntimePermission(packageName, permission);
169         } finally {
170             uiAutomation.dropShellPermissionIdentity();
171         }
172         try {
173             pollForPermission(packageName, permission, true);
174         } catch (Exception e) {
175             fail("Exception on polling for permission grant for " + packageName + " for "
176                     + permission + ": " + e.getMessage());
177         }
178     }
179 
180     /**
181      * Revokes permissions from the given package.
182      */
revokePermission(String packageName, String permission)183     public static void revokePermission(String packageName, String permission) {
184         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
185         uiAutomation.adoptShellPermissionIdentity("android.permission.REVOKE_RUNTIME_PERMISSIONS");
186         try {
187             uiAutomation.revokeRuntimePermission(packageName, permission);
188         } finally {
189             uiAutomation.dropShellPermissionIdentity();
190         }
191         try {
192             pollForPermission(packageName, permission, false);
193         } catch (Exception e) {
194             fail("Exception on polling for permission revoke for " + packageName + " for "
195                     + permission + ": " + e.getMessage());
196         }
197     }
198 
199     /**
200      * Adopts shell permission identity for the given permissions.
201      */
adoptShellPermissionIdentity(String... permissions)202     public static void adoptShellPermissionIdentity(String... permissions) {
203         InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
204                 permissions);
205     }
206 
207     /**
208      * Drops shell permission identity for all permissions.
209      */
dropShellPermissionIdentity()210     public static void dropShellPermissionIdentity() {
211         InstrumentationRegistry.getInstrumentation().getUiAutomation()
212                 .dropShellPermissionIdentity();
213     }
214 
215     /**
216      * Executes a shell command.
217      */
executeShellCommand(String pattern, Object...args)218     public static String executeShellCommand(String pattern, Object...args) throws IOException {
219         String command = String.format(pattern, args);
220         int attempt = 0;
221         while (attempt++ < 5) {
222             try {
223                 return executeShellCommandInternal(command);
224             } catch (InterruptedIOException e) {
225                 // Hmm, we had trouble executing the shell command; the best we
226                 // can do is try again a few more times
227                 Log.v(TAG, "Trouble executing " + command + "; trying again", e);
228             }
229         }
230         throw new IOException("Failed to execute " + command);
231     }
232 
executeShellCommandInternal(String cmd)233     private static String executeShellCommandInternal(String cmd) throws IOException {
234         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
235         try (FileInputStream output = new FileInputStream(
236                      uiAutomation.executeShellCommand(cmd).getFileDescriptor())) {
237             return new String(ByteStreams.toByteArray(output));
238         }
239     }
240 
241     /**
242      * Makes the given {@code testApp} list the content of the given directory and returns the
243      * result as an {@link ArrayList}
244      */
listAs(TestApp testApp, String dirPath)245     public static ArrayList<String> listAs(TestApp testApp, String dirPath) throws Exception {
246         return getContentsFromTestApp(testApp, dirPath, READDIR_QUERY);
247     }
248 
249     /**
250      * Returns {@code true} iff the given {@code path} exists and is readable and
251      * writable for for {@code testApp}.
252      */
canReadAndWriteAs(TestApp testApp, String path)253     public static boolean canReadAndWriteAs(TestApp testApp, String path) throws Exception {
254         return getResultFromTestApp(testApp, path, CAN_READ_WRITE_QUERY);
255     }
256 
257     /**
258      * Makes the given {@code testApp} read the EXIF metadata from the given file and returns the
259      * result as an {@link HashMap}
260      */
readExifMetadataFromTestApp( TestApp testApp, String filePath)261     public static HashMap<String, String> readExifMetadataFromTestApp(
262             TestApp testApp, String filePath) throws Exception {
263         HashMap<String, String> res =
264                 getMetadataFromTestApp(testApp, filePath, EXIF_METADATA_QUERY);
265         return res;
266     }
267 
268     /**
269      * Makes the given {@code testApp} create a file.
270      *
271      * <p>This method drops shell permission identity.
272      */
createFileAs(TestApp testApp, String path)273     public static boolean createFileAs(TestApp testApp, String path) throws Exception {
274         return getResultFromTestApp(testApp, path, CREATE_FILE_QUERY);
275     }
276 
277     /**
278      * Makes the given {@code testApp} create a mediastore DB entry under
279      * {@code MediaStore.Media.Images}.
280      *
281      * The {@code path} argument is treated as a relative path and a name separated
282      * by an {@code '/'}.
283      */
createImageEntryAs(TestApp testApp, String path)284     public static boolean createImageEntryAs(TestApp testApp, String path) throws Exception {
285         return getResultFromTestApp(testApp, path, CREATE_IMAGE_ENTRY_QUERY);
286     }
287 
288     /**
289      * Makes the given {@code testApp} delete a file.
290      *
291      * <p>This method drops shell permission identity.
292      */
deleteFileAs(TestApp testApp, String path)293     public static boolean deleteFileAs(TestApp testApp, String path) throws Exception {
294         return getResultFromTestApp(testApp, path, DELETE_FILE_QUERY);
295     }
296 
297     /**
298      * Makes the given {@code testApp} delete a file. Doesn't throw in case of failure.
299      */
deleteFileAsNoThrow(TestApp testApp, String path)300     public static boolean deleteFileAsNoThrow(TestApp testApp, String path) {
301         try {
302             return deleteFileAs(testApp, path);
303         } catch (Exception e) {
304             Log.e(TAG,
305                     "Error occurred while deleting file: " + path + " on behalf of app: " + testApp,
306                     e);
307             return false;
308         }
309     }
310 
311     /**
312      * Makes the given {@code testApp} open {@code file} for read or write.
313      *
314      * <p>This method drops shell permission identity.
315      */
canOpenFileAs(TestApp testApp, File file, boolean forWrite)316     public static boolean canOpenFileAs(TestApp testApp, File file, boolean forWrite)
317             throws Exception {
318         String actionName = forWrite ? CAN_OPEN_FILE_FOR_WRITE_QUERY : CAN_OPEN_FILE_FOR_READ_QUERY;
319         return getResultFromTestApp(testApp, file.getPath(), actionName);
320     }
321 
322     /**
323      * Makes the given {@code testApp} rename give {@code src} to {@code dst}.
324      *
325      * The method concatenates source and destination paths while sending the request to
326      * {@code testApp}. Hence, {@link TestUtils#RENAME_FILE_PARAMS_SEPARATOR} shouldn't be used
327      * in path names.
328      *
329      * <p>This method drops shell permission identity.
330      */
renameFileAs(TestApp testApp, File src, File dst)331     public static boolean renameFileAs(TestApp testApp, File src, File dst) throws Exception {
332         final String paths = String.format("%s%s%s",
333                 src.getAbsolutePath(), RENAME_FILE_PARAMS_SEPARATOR, dst.getAbsolutePath());
334         return getResultFromTestApp(testApp, paths, RENAME_FILE_QUERY);
335     }
336 
337     /**
338      * Makes the given {@code testApp} check if a database row exists for given {@code file}
339      *
340      * <p>This method drops shell permission identity.
341      */
checkDatabaseRowExistsAs(TestApp testApp, File file)342     public static boolean checkDatabaseRowExistsAs(TestApp testApp, File file) throws Exception {
343         return getResultFromTestApp(testApp, file.getPath(), CHECK_DATABASE_ROW_EXISTS_QUERY);
344     }
345 
346     /**
347      * Makes the given {@code testApp} open file descriptor on {@code uri} and verifies that the fd
348      * redacts EXIF metadata.
349      *
350      * <p> This method drops shell permission identity.
351      */
isFileDescriptorRedacted(TestApp testApp, Uri uri)352     public static boolean isFileDescriptorRedacted(TestApp testApp, Uri uri)
353             throws Exception {
354         String actionName = IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_READ;
355         return getFromTestApp(testApp, uri, actionName).getBoolean(actionName, false);
356     }
357 
358     /**
359      * Makes the given {@code testApp} open file descriptor on {@code uri} and verifies that the fd
360      * redacts EXIF metadata.
361      *
362      * <p> This method drops shell permission identity.
363      */
canOpenRedactedUriForWrite(TestApp testApp, Uri uri)364     public static boolean canOpenRedactedUriForWrite(TestApp testApp, Uri uri)
365             throws Exception {
366         String actionName = IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_WRITE;
367         return getFromTestApp(testApp, uri, actionName).getBoolean(actionName, false);
368     }
369 
370 
371     /**
372      * Makes the given {@code testApp} open file path associated with {@code uri} and verifies that
373      * the path redacts EXIF metadata.
374      *
375      * <p>This method drops shell permission identity.
376      */
isFileOpenRedacted(TestApp testApp, Uri uri)377     public static boolean isFileOpenRedacted(TestApp testApp, Uri uri)
378             throws Exception {
379         final String actionName = IS_URI_REDACTED_VIA_FILEPATH;
380         return getFromTestApp(testApp, uri, actionName).getBoolean(actionName, false);
381     }
382 
383     /**
384      * Makes the given {@code testApp} query on {@code uri}.
385      *
386      * <p>This method drops shell permission identity.
387      */
canQueryOnUri(TestApp testApp, Uri uri)388     public static boolean canQueryOnUri(TestApp testApp, Uri uri) throws Exception {
389         final String actionName = QUERY_URI;
390         return getFromTestApp(testApp, uri, actionName).getBoolean(actionName, false);
391     }
392 
insertFileFromExternalMedia(boolean useRelative)393     public static Uri insertFileFromExternalMedia(boolean useRelative) throws IOException {
394         ContentValues values = new ContentValues();
395         String filePath =
396                 getAndroidMediaDir().toString() + "/" + getContext().getPackageName() + "/"
397                         + System.currentTimeMillis();
398         if (useRelative) {
399             values.put(MediaStore.MediaColumns.RELATIVE_PATH,
400                     "Android/media/" + getContext().getPackageName());
401             values.put(MediaStore.MediaColumns.DISPLAY_NAME, System.currentTimeMillis());
402         } else {
403             values.put(MediaStore.MediaColumns.DATA, filePath);
404         }
405 
406         return getContentResolver().insert(
407                 MediaStore.Files.getContentUri(sStorageVolumeName), values);
408     }
409 
insertFile(ContentValues values)410     public static void insertFile(ContentValues values) {
411         assertNotNull(getContentResolver().insert(
412                 MediaStore.Files.getContentUri(sStorageVolumeName), values));
413     }
414 
updateFile(Uri uri, ContentValues values)415     public static int updateFile(Uri uri, ContentValues values) {
416         return getContentResolver().update(uri, values, new Bundle());
417     }
418 
verifyInsertFromExternalPrivateDirViaRelativePath_denied()419     public static void verifyInsertFromExternalPrivateDirViaRelativePath_denied() throws Exception {
420         resetDefaultExternalStorageVolume();
421 
422         // Test that inserting files from Android/obb/.. is not allowed.
423         final String androidObbDir = getContext().getObbDir().toString();
424         ContentValues values = new ContentValues();
425         values.put(
426                 MediaStore.MediaColumns.RELATIVE_PATH,
427                 androidObbDir.substring(androidObbDir.indexOf("Android")));
428         assertThrows(IllegalArgumentException.class, () -> insertFile(values));
429 
430         // Test that inserting files from Android/data/.. is not allowed.
431         final String androidDataDir = getExternalFilesDir().toString();
432         values.put(
433                 MediaStore.MediaColumns.RELATIVE_PATH,
434                 androidDataDir.substring(androidDataDir.indexOf("Android")));
435         assertThrows(IllegalArgumentException.class, () -> insertFile(values));
436     }
437 
verifyInsertFromExternalMediaDirViaRelativePath_allowed()438     public static void verifyInsertFromExternalMediaDirViaRelativePath_allowed() throws Exception {
439         resetDefaultExternalStorageVolume();
440 
441         // Test that inserting files from Android/media/.. is allowed.
442         final String androidMediaDir = getExternalMediaDir().toString();
443         final ContentValues values = new ContentValues();
444         values.put(
445                 MediaStore.MediaColumns.RELATIVE_PATH,
446                 androidMediaDir.substring(androidMediaDir.indexOf("Android")));
447         insertFile(values);
448     }
449 
verifyInsertFromExternalPrivateDirViaData_denied()450     public static void verifyInsertFromExternalPrivateDirViaData_denied() throws Exception {
451         resetDefaultExternalStorageVolume();
452 
453         ContentValues values = new ContentValues();
454 
455         // Test that inserting files from Android/obb/.. is not allowed.
456         final String androidObbDir =
457                 getContext().getObbDir().toString() + "/" + System.currentTimeMillis();
458         values.put(MediaStore.MediaColumns.DATA, androidObbDir);
459         assertThrows(IllegalArgumentException.class, () -> insertFile(values));
460 
461         // Test that inserting files from Android/data/.. is not allowed.
462         final String androidDataDir = getExternalFilesDir().toString();
463         values.put(MediaStore.MediaColumns.DATA, androidDataDir);
464         assertThrows(IllegalArgumentException.class, () -> insertFile(values));
465     }
466 
verifyInsertFromExternalMediaDirViaData_allowed()467     public static void verifyInsertFromExternalMediaDirViaData_allowed() throws Exception {
468         resetDefaultExternalStorageVolume();
469 
470         // Test that inserting files from Android/media/.. is allowed.
471         ContentValues values = new ContentValues();
472         final String androidMediaDirFile =
473                 getExternalMediaDir().toString() + "/" + System.currentTimeMillis();
474         values.put(MediaStore.MediaColumns.DATA, androidMediaDirFile);
475         insertFile(values);
476     }
477 
478     // NOTE: While updating, DATA field should be ignored for all the apps including file manager.
verifyUpdateToExternalDirsViaData_denied()479     public static void verifyUpdateToExternalDirsViaData_denied() throws Exception {
480         resetDefaultExternalStorageVolume();
481         Uri uri = insertFileFromExternalMedia(false);
482 
483         final String androidMediaDirFile =
484                 getExternalMediaDir().toString() + "/" + System.currentTimeMillis();
485         ContentValues values = new ContentValues();
486         values.put(MediaStore.MediaColumns.DATA, androidMediaDirFile);
487         assertEquals(0, updateFile(uri, values));
488 
489         final String androidObbDir =
490                 getContext().getObbDir().toString() + "/" + System.currentTimeMillis();
491         values.put(MediaStore.MediaColumns.DATA, androidObbDir);
492         assertEquals(0, updateFile(uri, values));
493 
494         final String androidDataDir = getExternalFilesDir().toString();
495         values.put(MediaStore.MediaColumns.DATA, androidDataDir);
496         assertEquals(0, updateFile(uri, values));
497     }
498 
verifyUpdateToExternalMediaDirViaRelativePath_allowed()499     public static void verifyUpdateToExternalMediaDirViaRelativePath_allowed()
500             throws IOException {
501         resetDefaultExternalStorageVolume();
502         Uri uri = insertFileFromExternalMedia(true);
503 
504         // Test that update to files from Android/media/.. is allowed.
505         final String androidMediaDir = getExternalMediaDir().toString();
506         ContentValues values = new ContentValues();
507         values.put(
508                 MediaStore.MediaColumns.RELATIVE_PATH,
509                 androidMediaDir.substring(androidMediaDir.indexOf("Android")));
510         assertNotEquals(0, updateFile(uri, values));
511     }
512 
verifyUpdateToExternalPrivateDirsViaRelativePath_denied()513     public static void verifyUpdateToExternalPrivateDirsViaRelativePath_denied()
514             throws Exception {
515         resetDefaultExternalStorageVolume();
516         Uri uri = insertFileFromExternalMedia(true);
517 
518         // Test that update to files from Android/obb/.. is not allowed.
519         final String androidObbDir = getContext().getObbDir().toString();
520         ContentValues values = new ContentValues();
521         values.put(
522                 MediaStore.MediaColumns.RELATIVE_PATH,
523                 androidObbDir.substring(androidObbDir.indexOf("Android")));
524         assertThrows(IllegalArgumentException.class, () -> updateFile(uri, values));
525 
526         // Test that update to files from Android/data/.. is not allowed.
527         final String androidDataDir = getExternalFilesDir().toString();
528         values.put(
529                 MediaStore.MediaColumns.RELATIVE_PATH,
530                 androidDataDir.substring(androidDataDir.indexOf("Android")));
531         assertThrows(IllegalArgumentException.class, () -> updateFile(uri, values));
532     }
533 
534     /**
535      * Makes the given {@code testApp} open a file for read or write.
536      *
537      * <p>This method drops shell permission identity.
538      */
openFileAs(TestApp testApp, File file, boolean forWrite)539     public static ParcelFileDescriptor openFileAs(TestApp testApp, File file, boolean forWrite)
540             throws Exception {
541         String actionName = forWrite ? OPEN_FILE_FOR_WRITE_QUERY : OPEN_FILE_FOR_READ_QUERY;
542         String mode = forWrite ? "rw" : "r";
543         return getPfdFromTestApp(testApp, file, actionName, mode);
544     }
545 
546     /**
547      * Makes the given {@code testApp} setattr for given file path.
548      *
549      * <p>This method drops shell permission identity.
550      */
setAttrAs(TestApp testApp, String path)551     public static boolean setAttrAs(TestApp testApp, String path)
552             throws Exception {
553         return getResultFromTestApp(testApp, path, SETATTR_QUERY);
554     }
555 
556     /**
557      * Installs a {@link TestApp} without storage permissions.
558      */
installApp(TestApp testApp)559     public static void installApp(TestApp testApp) throws Exception {
560         installApp(testApp, /* grantStoragePermission */ false);
561     }
562 
563     /**
564      * Installs a {@link TestApp} with storage permissions.
565      */
installAppWithStoragePermissions(TestApp testApp)566     public static void installAppWithStoragePermissions(TestApp testApp) throws Exception {
567         installApp(testApp, /* grantStoragePermission */ true);
568     }
569 
570     /**
571      * Installs a {@link TestApp} and may grant it storage permissions.
572      */
installApp(TestApp testApp, boolean grantStoragePermission)573     public static void installApp(TestApp testApp, boolean grantStoragePermission)
574             throws Exception {
575         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
576         try {
577             final String packageName = testApp.getPackageName();
578             uiAutomation.adoptShellPermissionIdentity(
579                     Manifest.permission.INSTALL_PACKAGES, Manifest.permission.DELETE_PACKAGES);
580             if (isAppInstalled(testApp)) {
581                 Uninstall.packages(packageName);
582             }
583             Install.single(testApp).commit();
584             assertThat(InstallUtils.getInstalledVersion(packageName)).isEqualTo(1);
585             if (grantStoragePermission) {
586                 grantPermission(packageName, Manifest.permission.READ_EXTERNAL_STORAGE);
587             }
588         } finally {
589             uiAutomation.dropShellPermissionIdentity();
590         }
591     }
592 
isAppInstalled(TestApp testApp)593     public static boolean isAppInstalled(TestApp testApp) {
594         return InstallUtils.getInstalledVersion(testApp.getPackageName()) != -1;
595     }
596 
597     /**
598      * Uninstalls a {@link TestApp}.
599      */
uninstallApp(TestApp testApp)600     public static void uninstallApp(TestApp testApp) throws Exception {
601         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
602         try {
603             final String packageName = testApp.getPackageName();
604             uiAutomation.adoptShellPermissionIdentity(Manifest.permission.DELETE_PACKAGES);
605 
606             Uninstall.packages(packageName);
607             assertThat(InstallUtils.getInstalledVersion(packageName)).isEqualTo(-1);
608         } finally {
609             uiAutomation.dropShellPermissionIdentity();
610         }
611     }
612 
613     /**
614      * Uninstalls a {@link TestApp}. Doesn't throw in case of failure.
615      */
uninstallAppNoThrow(TestApp testApp)616     public static void uninstallAppNoThrow(TestApp testApp) {
617         try {
618             uninstallApp(testApp);
619         } catch (Exception e) {
620             Log.e(TAG, "Exception occurred while uninstalling app: " + testApp, e);
621         }
622     }
623 
getContentResolver()624     public static ContentResolver getContentResolver() {
625         return getContext().getContentResolver();
626     }
627 
628     /**
629      * Inserts a file into the database using {@link MediaStore.MediaColumns#DATA}.
630      */
insertFileUsingDataColumn(@onNull File file)631     public static Uri insertFileUsingDataColumn(@NonNull File file) {
632         final ContentValues values = new ContentValues();
633         values.put(MediaStore.MediaColumns.DATA, file.getPath());
634         return getContentResolver().insert(MediaStore.Files.getContentUri(sStorageVolumeName),
635                 values);
636     }
637 
638     /**
639      * Returns the content URI for images based on the current storage volume.
640      */
getImageContentUri()641     public static Uri getImageContentUri() {
642         return MediaStore.Images.Media.getContentUri(sStorageVolumeName);
643     }
644 
645     /**
646      * Renames the given file using {@link ContentResolver} and {@link MediaStore} and APIs.
647      * This method uses the data column, and not all apps can use it.
648      * @see MediaStore.MediaColumns#DATA
649      */
renameWithMediaProvider(@onNull File oldPath, @NonNull File newPath)650     public static int renameWithMediaProvider(@NonNull File oldPath, @NonNull File newPath) {
651         ContentValues values = new ContentValues();
652         values.put(MediaStore.MediaColumns.DATA, newPath.getPath());
653         return getContentResolver().update(MediaStore.Files.getContentUri(sStorageVolumeName),
654                 values, /*where*/ MediaStore.MediaColumns.DATA + "=?",
655                 /*whereArgs*/ new String[] {oldPath.getPath()});
656     }
657 
658     /**
659      * Queries {@link ContentResolver} for a file and returns the corresponding {@link Uri} for its
660      * entry in the database. Returns {@code null} if file doesn't exist in the database.
661      */
662     @Nullable
getFileUri(@onNull File file)663     public static Uri getFileUri(@NonNull File file) {
664         final Uri contentUri = MediaStore.Files.getContentUri(sStorageVolumeName);
665         final int id = getFileRowIdFromDatabase(file);
666         return id == -1 ? null : ContentUris.withAppendedId(contentUri, id);
667     }
668 
669     /**
670      * Queries {@link ContentResolver} for a file and returns the corresponding row ID for its
671      * entry in the database. Returns {@code -1} if file is not found.
672      */
getFileRowIdFromDatabase(@onNull File file)673     public static int getFileRowIdFromDatabase(@NonNull File file) {
674         return getFileRowIdFromDatabase(getContentResolver(), file);
675     }
676 
677     /**
678      * Queries given {@link ContentResolver} for a file and returns the corresponding row ID for
679      * its entry in the database. Returns {@code -1} if file is not found.
680      */
getFileRowIdFromDatabase(ContentResolver cr, @NonNull File file)681     public static int getFileRowIdFromDatabase(ContentResolver cr, @NonNull File file) {
682         int id = -1;
683         try (Cursor c = queryFile(cr, file, MediaStore.MediaColumns._ID)) {
684             if (c.moveToFirst()) {
685                 id = c.getInt(0);
686             }
687         }
688         return id;
689     }
690 
691     /**
692      * Queries {@link ContentResolver} for a file and returns the corresponding owner package name
693      * for its entry in the database.
694      */
695     @Nullable
getFileOwnerPackageFromDatabase(@onNull File file)696     public static String getFileOwnerPackageFromDatabase(@NonNull File file) {
697         String ownerPackage = null;
698         try (Cursor c = queryFile(file, MediaStore.MediaColumns.OWNER_PACKAGE_NAME)) {
699             if (c.moveToFirst()) {
700                 ownerPackage = c.getString(0);
701             }
702         }
703         return ownerPackage;
704     }
705 
706     /**
707      * Queries {@link ContentResolver} for a file and returns the corresponding file size for its
708      * entry in the database. Returns {@code -1} if file is not found.
709      */
710     @Nullable
getFileSizeFromDatabase(@onNull File file)711     public static int getFileSizeFromDatabase(@NonNull File file) {
712         int size = -1;
713         try (Cursor c = queryFile(file, MediaStore.MediaColumns.SIZE)) {
714             if (c.moveToFirst()) {
715                 size = c.getInt(0);
716             }
717         }
718         return size;
719     }
720 
721     /**
722      * Queries {@link ContentResolver} for a video file and returns a {@link Cursor} with the given
723      * columns.
724      */
725     @NonNull
queryVideoFile(File file, String... projection)726     public static Cursor queryVideoFile(File file, String... projection) {
727         return queryFile(getContentResolver(),
728                 MediaStore.Video.Media.getContentUri(sStorageVolumeName), file,
729                 /*includePending*/ true, projection);
730     }
731 
732     /**
733      * Queries {@link ContentResolver} for an image file and returns a {@link Cursor} with the given
734      * columns.
735      */
736     @NonNull
queryImageFile(File file, String... projection)737     public static Cursor queryImageFile(File file, String... projection) {
738         return queryFile(getContentResolver(),
739                 MediaStore.Images.Media.getContentUri(sStorageVolumeName), file,
740                 /*includePending*/ true, projection);
741     }
742 
743     /**
744      * Queries {@link ContentResolver} for a file and returns the corresponding mime type for its
745      * entry in the database.
746      */
747     @NonNull
getFileMimeTypeFromDatabase(@onNull File file)748     public static String getFileMimeTypeFromDatabase(@NonNull File file) {
749         String mimeType = "";
750         try (Cursor c = queryFile(file, MediaStore.MediaColumns.MIME_TYPE)) {
751             if (c.moveToFirst()) {
752                 mimeType = c.getString(0);
753             }
754         }
755         return mimeType;
756     }
757 
758     /**
759      * Sets {@link AppOpsManager#MODE_ALLOWED} for the given {@code ops} and the given {@code uid}.
760      *
761      * <p>This method drops shell permission identity.
762      */
allowAppOpsToUid(int uid, @NonNull String... ops)763     public static void allowAppOpsToUid(int uid, @NonNull String... ops) {
764         setAppOpsModeForUid(uid, AppOpsManager.MODE_ALLOWED, ops);
765     }
766 
767     /**
768      * Sets {@link AppOpsManager#MODE_ERRORED} for the given {@code ops} and the given {@code uid}.
769      *
770      * <p>This method drops shell permission identity.
771      */
denyAppOpsToUid(int uid, @NonNull String... ops)772     public static void denyAppOpsToUid(int uid, @NonNull String... ops) {
773         setAppOpsModeForUid(uid, AppOpsManager.MODE_ERRORED, ops);
774     }
775 
776     /**
777      * Deletes the given file through {@link ContentResolver} and {@link MediaStore} APIs,
778      * and asserts that the file was successfully deleted from the database.
779      */
deleteWithMediaProvider(@onNull File file)780     public static void deleteWithMediaProvider(@NonNull File file) {
781         Bundle extras = new Bundle();
782         extras.putString(ContentResolver.QUERY_ARG_SQL_SELECTION,
783                 MediaStore.MediaColumns.DATA + " = ?");
784         extras.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
785                 new String[] {file.getPath()});
786         extras.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_INCLUDE);
787         extras.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE);
788         assertThat(getContentResolver().delete(
789                 MediaStore.Files.getContentUri(sStorageVolumeName), extras)).isEqualTo(1);
790     }
791 
792     /**
793      * Deletes db rows and files corresponding to uri through {@link ContentResolver} and
794      * {@link MediaStore} APIs.
795      */
deleteWithMediaProviderNoThrow(Uri... uris)796     public static void deleteWithMediaProviderNoThrow(Uri... uris) {
797         for (Uri uri : uris) {
798             if (uri == null) continue;
799 
800             try {
801                 getContentResolver().delete(uri, Bundle.EMPTY);
802             } catch (Exception ignored) {
803             }
804         }
805     }
806 
807     /**
808      * Renames the given file through {@link ContentResolver} and {@link MediaStore} APIs,
809      * and asserts that the file was updated in the database.
810      */
updateDisplayNameWithMediaProvider(Uri uri, String relativePath, String oldDisplayName, String newDisplayName)811     public static void updateDisplayNameWithMediaProvider(Uri uri, String relativePath,
812             String oldDisplayName, String newDisplayName) {
813         String selection = MediaStore.MediaColumns.RELATIVE_PATH + " = ? AND "
814                 + MediaStore.MediaColumns.DISPLAY_NAME + " = ?";
815         String[] selectionArgs = {relativePath + '/', oldDisplayName};
816         Bundle extras = new Bundle();
817         extras.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, selection);
818         extras.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, selectionArgs);
819         extras.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_INCLUDE);
820         extras.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE);
821 
822         ContentValues values = new ContentValues();
823         values.put(MediaStore.MediaColumns.DISPLAY_NAME, newDisplayName);
824 
825         assertThat(getContentResolver().update(uri, values, extras)).isEqualTo(1);
826     }
827 
828     /**
829      * Opens the given file through {@link ContentResolver} and {@link MediaStore} APIs.
830      */
831     @NonNull
openWithMediaProvider(@onNull File file, String mode)832     public static ParcelFileDescriptor openWithMediaProvider(@NonNull File file, String mode)
833             throws Exception {
834         final Uri fileUri = getFileUri(file);
835         assertThat(fileUri).isNotNull();
836         Log.i(TAG, "Uri: " + fileUri + ". Data: " + file.getPath());
837         ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(fileUri, mode);
838         assertThat(pfd).isNotNull();
839         return pfd;
840     }
841 
842     /**
843      * Opens the given file via file path
844      */
845     @NonNull
openWithFilePath(File file, boolean forWrite)846     public static ParcelFileDescriptor openWithFilePath(File file, boolean forWrite)
847             throws IOException {
848         return ParcelFileDescriptor.open(file,
849                 forWrite
850                 ? ParcelFileDescriptor.MODE_READ_WRITE : ParcelFileDescriptor.MODE_READ_ONLY);
851     }
852 
853     /**
854      * Returns whether we can open the file.
855      */
canOpen(File file, boolean forWrite)856     public static boolean canOpen(File file, boolean forWrite) {
857         try {
858             openWithFilePath(file, forWrite);
859             return true;
860         } catch (IOException expected) {
861             return false;
862         }
863     }
864 
865     /**
866      * Asserts the given operation throws an exception of type {@code T}.
867      */
assertThrows(Class<T> clazz, Operation<Exception> r)868     public static <T extends Exception> void assertThrows(Class<T> clazz, Operation<Exception> r)
869             throws Exception {
870         assertThrows(clazz, "", r);
871     }
872 
873     /**
874      * Asserts the given operation throws an exception of type {@code T}.
875      */
assertThrows( Class<T> clazz, String errMsg, Operation<Exception> r)876     public static <T extends Exception> void assertThrows(
877             Class<T> clazz, String errMsg, Operation<Exception> r) throws Exception {
878         try {
879             r.run();
880             fail("Expected " + clazz + " to be thrown");
881         } catch (Exception e) {
882             if (!clazz.isAssignableFrom(e.getClass()) || !e.getMessage().contains(errMsg)) {
883                 Log.e(TAG, "Expected " + clazz + " exception with error message: " + errMsg, e);
884                 throw e;
885             }
886         }
887     }
888 
setShouldForceStopTestApp(boolean value)889     public static void setShouldForceStopTestApp(boolean value) {
890         sShouldForceStopTestApp = value;
891     }
892 
893     /**
894      * A functional interface representing an operation that takes no arguments,
895      * returns no arguments and might throw an {@link Exception} of any kind.
896      *
897      * @param T the subclass of {@link java.lang.Exception} that this operation might throw.
898      */
899     @FunctionalInterface
900     public interface Operation<T extends Exception> {
901         /**
902          * This is the method that gets called for any object that implements this interface.
903          */
run()904         void run() throws T;
905     }
906 
907     /**
908      * Deletes the given file. If the file is a directory, then deletes all of its children (files
909      * or directories) recursively.
910      */
deleteRecursively(@onNull File path)911     public static boolean deleteRecursively(@NonNull File path) {
912         if (path.isDirectory()) {
913             for (File child : path.listFiles()) {
914                 if (!deleteRecursively(child)) {
915                     return false;
916                 }
917             }
918         }
919         return path.delete();
920     }
921 
922     /**
923      * Asserts can rename file.
924      */
assertCanRenameFile(File oldFile, File newFile)925     public static void assertCanRenameFile(File oldFile, File newFile) {
926         assertCanRenameFile(oldFile, newFile, /* checkDB */ true);
927     }
928 
929     /**
930      * Asserts can rename file and optionally checks if the database is updated after rename.
931      */
assertCanRenameFile(File oldFile, File newFile, boolean checkDatabase)932     public static void assertCanRenameFile(File oldFile, File newFile, boolean checkDatabase) {
933         assertThat(oldFile.renameTo(newFile)).isTrue();
934         assertThat(oldFile.exists()).isFalse();
935         assertThat(newFile.exists()).isTrue();
936         if (checkDatabase) {
937             assertThat(getFileRowIdFromDatabase(oldFile)).isEqualTo(-1);
938             assertThat(getFileRowIdFromDatabase(newFile)).isNotEqualTo(-1);
939         }
940     }
941 
942     /**
943      * Asserts cannot rename file.
944      */
assertCantRenameFile(File oldFile, File newFile)945     public static void assertCantRenameFile(File oldFile, File newFile) {
946         final int rowId = getFileRowIdFromDatabase(oldFile);
947         assertThat(oldFile.renameTo(newFile)).isFalse();
948         assertThat(oldFile.exists()).isTrue();
949         assertThat(getFileRowIdFromDatabase(oldFile)).isEqualTo(rowId);
950     }
951 
952     /**
953      * Asserts can rename directory.
954      */
assertCanRenameDirectory(File oldDirectory, File newDirectory, @Nullable File[] oldFilesList, @Nullable File[] newFilesList)955     public static void assertCanRenameDirectory(File oldDirectory, File newDirectory,
956             @Nullable File[] oldFilesList, @Nullable File[] newFilesList) {
957         assertThat(oldDirectory.renameTo(newDirectory)).isTrue();
958         assertThat(oldDirectory.exists()).isFalse();
959         assertThat(newDirectory.exists()).isTrue();
960         for (File file : oldFilesList != null ? oldFilesList : new File[0]) {
961             assertThat(file.exists()).isFalse();
962             assertThat(getFileRowIdFromDatabase(file)).isEqualTo(-1);
963         }
964         for (File file : newFilesList != null ? newFilesList : new File[0]) {
965             assertThat(file.exists()).isTrue();
966             assertThat(getFileRowIdFromDatabase(file)).isNotEqualTo(-1);
967         }
968     }
969 
970     /**
971      * Asserts cannot rename directory.
972      */
assertCantRenameDirectory( File oldDirectory, File newDirectory, @Nullable File[] oldFilesList)973     public static void assertCantRenameDirectory(
974             File oldDirectory, File newDirectory, @Nullable File[] oldFilesList) {
975         assertThat(oldDirectory.renameTo(newDirectory)).isFalse();
976         assertThat(oldDirectory.exists()).isTrue();
977         for (File file : oldFilesList != null ? oldFilesList : new File[0]) {
978             assertThat(file.exists()).isTrue();
979             assertThat(getFileRowIdFromDatabase(file)).isNotEqualTo(-1);
980         }
981     }
982 
assertMountMode(String packageName, int uid, int expectedMountMode)983     public static void assertMountMode(String packageName, int uid, int expectedMountMode) {
984         adoptShellPermissionIdentity("android.permission.WRITE_MEDIA_STORAGE");
985         try {
986             final StorageManager storageManager = getContext().getSystemService(
987                     StorageManager.class);
988             final int actualMountMode = storageManager.getExternalStorageMountMode(uid,
989                     packageName);
990             assertWithMessage("mount mode (%s=%s, %s=%s) for package %s and uid %s",
991                     expectedMountMode, mountModeToString(expectedMountMode),
992                     actualMountMode, mountModeToString(actualMountMode),
993                     packageName, uid).that(actualMountMode).isEqualTo(expectedMountMode);
994         } finally {
995             dropShellPermissionIdentity();
996         }
997     }
998 
mountModeToString(int mountMode)999     public static String mountModeToString(int mountMode) {
1000         switch (mountMode) {
1001             case 0:
1002                 return "EXTERNAL_NONE";
1003             case 1:
1004                 return "DEFAULT";
1005             case 2:
1006                 return "INSTALLER";
1007             case 3:
1008                 return "PASS_THROUGH";
1009             case 4:
1010                 return "ANDROID_WRITABLE";
1011             default:
1012                 return "INVALID(" + mountMode + ")";
1013         }
1014     }
1015 
assertCanAccessPrivateAppAndroidDataDir(boolean canAccess, TestApp testApp, String callingPackage, String fileName)1016     public static void assertCanAccessPrivateAppAndroidDataDir(boolean canAccess,
1017             TestApp testApp, String callingPackage, String fileName) throws Exception {
1018         File[] dataDirs = getContext().getExternalFilesDirs(null);
1019         canReadWriteFilesInDirs(dataDirs, canAccess, testApp, callingPackage, fileName);
1020     }
1021 
assertCanAccessPrivateAppAndroidObbDir(boolean canAccess, TestApp testApp, String callingPackage, String fileName)1022     public static void assertCanAccessPrivateAppAndroidObbDir(boolean canAccess,
1023             TestApp testApp, String callingPackage, String fileName) throws Exception {
1024         File[] obbDirs = getContext().getObbDirs();
1025         canReadWriteFilesInDirs(obbDirs, canAccess, testApp, callingPackage, fileName);
1026     }
1027 
canReadWriteFilesInDirs(File[] dirs, boolean canAccess, TestApp testApp, String callingPackage, String fileName)1028     private static void canReadWriteFilesInDirs(File[] dirs, boolean canAccess, TestApp testApp,
1029             String callingPackage, String fileName) throws Exception {
1030         for (File dir : dirs) {
1031             final File otherAppExternalDataDir = new File(dir.getPath().replace(
1032                     callingPackage, testApp.getPackageName()));
1033             final File file = new File(otherAppExternalDataDir, fileName);
1034             try {
1035                 assertThat(file.exists()).isFalse();
1036 
1037                 assertThat(createFileAs(testApp, file.getPath())).isTrue();
1038                 if (canAccess) {
1039                     assertThat(file.canRead()).isTrue();
1040                     assertThat(file.canWrite()).isTrue();
1041                 } else {
1042                     assertThat(file.canRead()).isFalse();
1043                     assertThat(file.canWrite()).isFalse();
1044                 }
1045             } finally {
1046                 deleteFileAsNoThrow(testApp, file.getAbsolutePath());
1047             }
1048         }
1049     }
1050 
1051     /**
1052      * Polls for external storage to be mounted.
1053      */
pollForExternalStorageState()1054     public static void pollForExternalStorageState() throws Exception {
1055         pollForCondition(
1056                 () -> Environment.getExternalStorageState(getExternalStorageDir())
1057                         .equals(Environment.MEDIA_MOUNTED),
1058                 "Timed out while waiting for ExternalStorageState to be MEDIA_MOUNTED");
1059     }
1060 
1061     /**
1062      * Polls until we're granted or denied a given permission.
1063      */
pollForPermission(String perm, boolean granted)1064     public static void pollForPermission(String perm, boolean granted) throws Exception {
1065         pollForCondition(() -> granted == checkPermissionAndAppOp(perm),
1066                 "Timed out while waiting for permission " + perm + " to be "
1067                         + (granted ? "granted" : "revoked"));
1068     }
1069 
1070     /**
1071      * Polls until {@code app} is granted or denied the given permission.
1072      */
pollForPermission(TestApp app, String perm, boolean granted)1073     public static void pollForPermission(TestApp app, String perm, boolean granted)
1074             throws Exception {
1075         pollForPermission(app.getPackageName(), perm, granted);
1076     }
1077 
1078     /**
1079      * Polls until {@code packageName} is granted or denied the given permission.
1080      */
pollForPermission(String packageName, String perm, boolean granted)1081     public static void pollForPermission(String packageName, String perm, boolean granted)
1082             throws Exception {
1083         pollForCondition(
1084                 () -> granted == checkPermission(packageName, perm),
1085                 "Timed out while waiting for permission " + perm + " to be "
1086                         + (granted ? "granted" : "revoked"));
1087     }
1088 
1089     /**
1090      * Returns true iff {@code packageName} is granted a given permission.
1091      */
checkPermission(String packageName, String perm)1092     public static boolean checkPermission(String packageName, String perm) {
1093         try {
1094             int uid = getContext().getPackageManager().getPackageUid(packageName, 0);
1095 
1096             Optional<ActivityManager.RunningAppProcessInfo> process = getAppProcessInfo(
1097                     packageName);
1098             int pid = process.isPresent() ? process.get().pid : -1;
1099             return checkPermissionAndAppOp(perm, packageName, pid, uid);
1100         } catch (PackageManager.NameNotFoundException e) {
1101             return false;
1102         }
1103     }
1104 
1105     /**
1106      * Returns true iff {@code app} is granted a given permission.
1107      */
checkPermission(TestApp app, String perm)1108     public static boolean checkPermission(TestApp app, String perm) {
1109         return checkPermission(app.getPackageName(), perm);
1110     }
1111 
1112     /**
1113      * Asserts the entire content of the file equals exactly {@code expectedContent}.
1114      */
assertFileContent(File file, byte[] expectedContent)1115     public static void assertFileContent(File file, byte[] expectedContent) throws IOException {
1116         try (FileInputStream fis = new FileInputStream(file)) {
1117             assertInputStreamContent(fis, expectedContent);
1118         }
1119     }
1120 
1121     /**
1122      * Asserts the entire content of the file equals exactly {@code expectedContent}.
1123      * <p>Sets {@code fd} to beginning of file first.
1124      */
assertFileContent(FileDescriptor fd, byte[] expectedContent)1125     public static void assertFileContent(FileDescriptor fd, byte[] expectedContent)
1126             throws IOException, ErrnoException {
1127         Os.lseek(fd, 0, OsConstants.SEEK_SET);
1128         try (FileInputStream fis = new FileInputStream(fd)) {
1129             assertInputStreamContent(fis, expectedContent);
1130         }
1131     }
1132 
1133     /**
1134      * Asserts that {@code dir} is a directory and that it doesn't contain any of
1135      * {@code unexpectedContent}
1136      */
assertDirectoryDoesNotContain(@onNull File dir, File... unexpectedContent)1137     public static void assertDirectoryDoesNotContain(@NonNull File dir, File... unexpectedContent) {
1138         assertThat(dir.isDirectory()).isTrue();
1139         assertThat(Arrays.asList(dir.listFiles())).containsNoneIn(unexpectedContent);
1140     }
1141 
1142     /**
1143      * Asserts that {@code dir} is a directory and that it contains all of {@code expectedContent}
1144      */
assertDirectoryContains(@onNull File dir, File... expectedContent)1145     public static void assertDirectoryContains(@NonNull File dir, File... expectedContent) {
1146         assertThat(dir.isDirectory()).isTrue();
1147         assertThat(Arrays.asList(dir.listFiles())).containsAtLeastElementsIn(expectedContent);
1148     }
1149 
getExternalStorageDir()1150     public static File getExternalStorageDir() {
1151         return sExternalStorageDirectory;
1152     }
1153 
setExternalStorageVolume(@onNull String volName)1154     public static void setExternalStorageVolume(@NonNull String volName) {
1155         sStorageVolumeName = volName.toLowerCase(Locale.ROOT);
1156         sExternalStorageDirectory = new File("/storage/" + volName);
1157     }
1158 
1159     /**
1160      * Resets the root directory of external storage to the default.
1161      *
1162      * @see Environment#getExternalStorageDirectory()
1163      */
resetDefaultExternalStorageVolume()1164     public static void resetDefaultExternalStorageVolume() {
1165         sStorageVolumeName = MediaStore.VOLUME_EXTERNAL;
1166         sExternalStorageDirectory = Environment.getExternalStorageDirectory();
1167     }
1168 
1169     /**
1170      * Asserts the default volume used in helper methods is the primary volume.
1171      */
assertDefaultVolumeIsPrimary()1172     public static void assertDefaultVolumeIsPrimary() {
1173         assertVolumeType(true /* isPrimary */);
1174     }
1175 
1176     /**
1177      * Asserts the default volume used in helper methods is a public volume.
1178      */
assertDefaultVolumeIsPublic()1179     public static void assertDefaultVolumeIsPublic() {
1180         assertVolumeType(false /* isPrimary */);
1181     }
1182 
1183     /**
1184      * Creates and returns the Android data sub-directory belonging to the calling package.
1185      */
getExternalFilesDir()1186     public static File getExternalFilesDir() {
1187         final String packageName = getContext().getPackageName();
1188         final File res = new File(getAndroidDataDir(), packageName + "/files");
1189         if (!res.equals(getContext().getExternalFilesDir(null))) {
1190             res.mkdirs();
1191         }
1192         return res;
1193     }
1194 
1195     /**
1196      * Creates and returns the Android media sub-directory belonging to the calling package.
1197      */
getExternalMediaDir()1198     public static File getExternalMediaDir() {
1199         final String packageName = getContext().getPackageName();
1200         final File res = new File(getAndroidMediaDir(), packageName);
1201         if (!res.equals(getContext().getExternalMediaDirs()[0])) {
1202             res.mkdirs();
1203         }
1204         return res;
1205     }
1206 
getAlarmsDir()1207     public static File getAlarmsDir() {
1208         return new File(getExternalStorageDir(),
1209                 Environment.DIRECTORY_ALARMS);
1210     }
1211 
getAndroidDir()1212     public static File getAndroidDir() {
1213         return new File(getExternalStorageDir(),
1214                 "Android");
1215     }
1216 
getAudiobooksDir()1217     public static File getAudiobooksDir() {
1218         return new File(getExternalStorageDir(),
1219                 Environment.DIRECTORY_AUDIOBOOKS);
1220     }
1221 
getDcimDir()1222     public static File getDcimDir() {
1223         return new File(getExternalStorageDir(), Environment.DIRECTORY_DCIM);
1224     }
1225 
getDocumentsDir()1226     public static File getDocumentsDir() {
1227         return new File(getExternalStorageDir(),
1228                 Environment.DIRECTORY_DOCUMENTS);
1229     }
1230 
getDownloadDir()1231     public static File getDownloadDir() {
1232         return new File(getExternalStorageDir(),
1233                 Environment.DIRECTORY_DOWNLOADS);
1234     }
1235 
getMusicDir()1236     public static File getMusicDir() {
1237         return new File(getExternalStorageDir(),
1238                 Environment.DIRECTORY_MUSIC);
1239     }
1240 
getMoviesDir()1241     public static File getMoviesDir() {
1242         return new File(getExternalStorageDir(),
1243                 Environment.DIRECTORY_MOVIES);
1244     }
1245 
getNotificationsDir()1246     public static File getNotificationsDir() {
1247         return new File(getExternalStorageDir(),
1248                 Environment.DIRECTORY_NOTIFICATIONS);
1249     }
1250 
getPicturesDir()1251     public static File getPicturesDir() {
1252         return new File(getExternalStorageDir(),
1253                 Environment.DIRECTORY_PICTURES);
1254     }
1255 
getPodcastsDir()1256     public static File getPodcastsDir() {
1257         return new File(getExternalStorageDir(),
1258                 Environment.DIRECTORY_PODCASTS);
1259     }
1260 
getRecordingsDir()1261     public static File getRecordingsDir() {
1262         return new File(getExternalStorageDir(),
1263                 Environment.DIRECTORY_RECORDINGS);
1264     }
1265 
getRingtonesDir()1266     public static File getRingtonesDir() {
1267         return new File(getExternalStorageDir(),
1268                 Environment.DIRECTORY_RINGTONES);
1269     }
1270 
getAndroidDataDir()1271     public static File getAndroidDataDir() {
1272         return new File(getAndroidDir(), "data");
1273     }
1274 
getAndroidMediaDir()1275     public static File getAndroidMediaDir() {
1276         return new File(getAndroidDir(), "media");
1277     }
1278 
getDefaultTopLevelDirs()1279     public static File[] getDefaultTopLevelDirs() {
1280         if (BuildCompat.isAtLeastS()) {
1281             return new File[]{getAlarmsDir(), getAndroidDir(), getAudiobooksDir(), getDcimDir(),
1282                     getDocumentsDir(), getDownloadDir(), getMusicDir(), getMoviesDir(),
1283                     getNotificationsDir(), getPicturesDir(), getPodcastsDir(), getRecordingsDir(),
1284                     getRingtonesDir()};
1285         }
1286         return new File[]{getAlarmsDir(), getAndroidDir(), getAudiobooksDir(), getDcimDir(),
1287             getDocumentsDir(), getDownloadDir(), getMusicDir(), getMoviesDir(),
1288             getNotificationsDir(), getPicturesDir(), getPodcastsDir(),
1289             getRingtonesDir()};
1290     }
1291 
assertInputStreamContent(InputStream in, byte[] expectedContent)1292     private static void assertInputStreamContent(InputStream in, byte[] expectedContent)
1293             throws IOException {
1294         assertThat(ByteStreams.toByteArray(in)).isEqualTo(expectedContent);
1295     }
1296 
1297     /**
1298      * Checks if the given {@code permission} is granted and corresponding AppOp is MODE_ALLOWED.
1299      */
checkPermissionAndAppOp(String permission)1300     private static boolean checkPermissionAndAppOp(String permission) {
1301         final int pid = Os.getpid();
1302         final int uid = Os.getuid();
1303         final String packageName = getContext().getPackageName();
1304         return checkPermissionAndAppOp(permission, packageName, pid, uid);
1305     }
1306 
1307     /**
1308      * Checks if the given {@code permission} is granted and corresponding AppOp is MODE_ALLOWED.
1309      */
checkPermissionAndAppOp(String permission, String packageName, int pid, int uid)1310     private static boolean checkPermissionAndAppOp(String permission, String packageName, int pid,
1311             int uid) {
1312         final Context context = getContext();
1313         if (context.checkPermission(permission, pid, uid) != PackageManager.PERMISSION_GRANTED) {
1314             return false;
1315         }
1316 
1317         final String op = AppOpsManager.permissionToOp(permission);
1318         // No AppOp associated with the given permission, skip AppOp check.
1319         if (op == null) {
1320             return true;
1321         }
1322 
1323         final AppOpsManager appOps = context.getSystemService(AppOpsManager.class);
1324         try {
1325             appOps.checkPackage(uid, packageName);
1326         } catch (SecurityException e) {
1327             return false;
1328         }
1329 
1330         return appOps.unsafeCheckOpNoThrow(op, uid, packageName) == AppOpsManager.MODE_ALLOWED;
1331     }
1332 
1333     /**
1334      * <p>This method drops shell permission identity.
1335      */
forceStopApp(String packageName)1336     public static void forceStopApp(String packageName) throws Exception {
1337         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
1338         try {
1339             uiAutomation.adoptShellPermissionIdentity(Manifest.permission.FORCE_STOP_PACKAGES);
1340 
1341             getContext().getSystemService(ActivityManager.class).forceStopPackage(packageName);
1342             pollForCondition(() -> {
1343                 return !isProcessRunning(packageName);
1344             }, "Timed out while waiting for " + packageName + " to be stopped");
1345         } finally {
1346             uiAutomation.dropShellPermissionIdentity();
1347         }
1348     }
1349 
launchTestApp(TestApp testApp, String actionName, BroadcastReceiver broadcastReceiver, CountDownLatch latch, Intent intent)1350     private static void launchTestApp(TestApp testApp, String actionName,
1351             BroadcastReceiver broadcastReceiver, CountDownLatch latch, Intent intent)
1352             throws InterruptedException, TimeoutException {
1353 
1354         // Register broadcast receiver
1355         final IntentFilter intentFilter = new IntentFilter();
1356         intentFilter.addAction(actionName);
1357         intentFilter.addCategory(Intent.CATEGORY_DEFAULT);
1358         getContext().registerReceiver(broadcastReceiver, intentFilter);
1359 
1360         // Launch the test app.
1361         intent.setPackage(testApp.getPackageName());
1362         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1363         intent.putExtra(QUERY_TYPE, actionName);
1364         intent.putExtra(INTENT_EXTRA_CALLING_PKG, getContext().getPackageName());
1365         intent.addCategory(Intent.CATEGORY_LAUNCHER);
1366         getContext().startActivity(intent);
1367         if (!latch.await(POLLING_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
1368             final String errorMessage = "Timed out while waiting to receive " + actionName
1369                     + " intent from " + testApp.getPackageName();
1370             throw new TimeoutException(errorMessage);
1371         }
1372         getContext().unregisterReceiver(broadcastReceiver);
1373     }
1374 
1375     /**
1376      * Sends intent to {@code testApp} for actions on {@code dirPath}
1377      *
1378      * <p>This method drops shell permission identity.
1379      */
sendIntentToTestApp(TestApp testApp, String dirPath, String actionName, BroadcastReceiver broadcastReceiver, CountDownLatch latch)1380     private static void sendIntentToTestApp(TestApp testApp, String dirPath, String actionName,
1381             BroadcastReceiver broadcastReceiver, CountDownLatch latch) throws Exception {
1382         if (sShouldForceStopTestApp) {
1383             final String packageName = testApp.getPackageName();
1384             forceStopApp(packageName);
1385         }
1386 
1387         // Launch the test app.
1388         final Intent intent = new Intent(Intent.ACTION_MAIN);
1389         intent.putExtra(INTENT_EXTRA_PATH, dirPath);
1390         launchTestApp(testApp, actionName, broadcastReceiver, latch, intent);
1391     }
1392 
1393     /**
1394      * Sends intent to {@code testApp} for actions on {@code uri}
1395      *
1396      * <p>This method drops shell permission identity.
1397      */
sendIntentToTestApp(TestApp testApp, Uri uri, String actionName, BroadcastReceiver broadcastReceiver, CountDownLatch latch)1398     private static void sendIntentToTestApp(TestApp testApp, Uri uri, String actionName,
1399             BroadcastReceiver broadcastReceiver, CountDownLatch latch) throws Exception {
1400         if (sShouldForceStopTestApp) {
1401             final String packageName = testApp.getPackageName();
1402             forceStopApp(packageName);
1403         }
1404 
1405         final Intent intent = new Intent(Intent.ACTION_MAIN);
1406         intent.putExtra(INTENT_EXTRA_URI, uri);
1407         launchTestApp(testApp, actionName, broadcastReceiver, latch, intent);
1408     }
1409 
1410     /**
1411      * Gets images/video metadata from a test app.
1412      *
1413      * <p>This method drops shell permission identity.
1414      */
getMetadataFromTestApp( TestApp testApp, String dirPath, String actionName)1415     private static HashMap<String, String> getMetadataFromTestApp(
1416             TestApp testApp, String dirPath, String actionName) throws Exception {
1417         Bundle bundle = getFromTestApp(testApp, dirPath, actionName);
1418         return (HashMap<String, String>) bundle.get(actionName);
1419     }
1420 
1421     /**
1422      * <p>This method drops shell permission identity.
1423      */
getContentsFromTestApp( TestApp testApp, String dirPath, String actionName)1424     private static ArrayList<String> getContentsFromTestApp(
1425             TestApp testApp, String dirPath, String actionName) throws Exception {
1426         Bundle bundle = getFromTestApp(testApp, dirPath, actionName);
1427         return bundle.getStringArrayList(actionName);
1428     }
1429 
1430     /**
1431      * <p>This method drops shell permission identity.
1432      */
getResultFromTestApp(TestApp testApp, String dirPath, String actionName)1433     private static boolean getResultFromTestApp(TestApp testApp, String dirPath, String actionName)
1434             throws Exception {
1435         Bundle bundle = getFromTestApp(testApp, dirPath, actionName);
1436         return bundle.getBoolean(actionName, false);
1437     }
1438 
getPfdFromTestApp(TestApp testApp, File dirPath, String actionName, String mode)1439     private static ParcelFileDescriptor getPfdFromTestApp(TestApp testApp, File dirPath,
1440             String actionName, String mode) throws Exception {
1441         Bundle bundle = getFromTestApp(testApp, dirPath.getPath(), actionName);
1442         return getContentResolver().openFileDescriptor(bundle.getParcelable(actionName), mode);
1443     }
1444 
1445     /**
1446      * <p>This method drops shell permission identity.
1447      */
getFromTestApp(TestApp testApp, String dirPath, String actionName)1448     private static Bundle getFromTestApp(TestApp testApp, String dirPath, String actionName)
1449             throws Exception {
1450         final CountDownLatch latch = new CountDownLatch(1);
1451         final Bundle[] bundle = new Bundle[1];
1452         final Exception[] exception = new Exception[1];
1453         exception[0] = null;
1454         BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
1455             @Override
1456             public void onReceive(Context context, Intent intent) {
1457                 if (intent.hasExtra(INTENT_EXCEPTION)) {
1458                     exception[0] = (Exception) (intent.getSerializableExtra(INTENT_EXCEPTION));
1459                 } else {
1460                     bundle[0] = intent.getExtras();
1461                 }
1462                 latch.countDown();
1463             }
1464         };
1465 
1466         sendIntentToTestApp(testApp, dirPath, actionName, broadcastReceiver, latch);
1467         if (exception[0] != null) {
1468             throw exception[0];
1469         }
1470         return bundle[0];
1471     }
1472 
1473     /**
1474      * <p>This method drops shell permission identity.
1475      */
getFromTestApp(TestApp testApp, Uri uri, String actionName)1476     private static Bundle getFromTestApp(TestApp testApp, Uri uri, String actionName)
1477             throws Exception {
1478         final CountDownLatch latch = new CountDownLatch(1);
1479         final Bundle[] bundle = new Bundle[1];
1480         final Exception[] exception = new Exception[1];
1481         exception[0] = null;
1482         BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
1483             @Override
1484             public void onReceive(Context context, Intent intent) {
1485                 if (intent.hasExtra(INTENT_EXCEPTION)) {
1486                     exception[0] = (Exception) (intent.getSerializableExtra(INTENT_EXCEPTION));
1487                 } else {
1488                     bundle[0] = intent.getExtras();
1489                 }
1490                 latch.countDown();
1491             }
1492         };
1493 
1494         sendIntentToTestApp(testApp, uri, actionName, broadcastReceiver, latch);
1495         if (exception[0] != null) {
1496             throw exception[0];
1497         }
1498         return bundle[0];
1499     }
1500 
1501     /**
1502      * Sets {@code mode} for the given {@code ops} and the given {@code uid}.
1503      *
1504      * <p>This method drops shell permission identity.
1505      */
setAppOpsModeForUid(int uid, int mode, @NonNull String... ops)1506     public static void setAppOpsModeForUid(int uid, int mode, @NonNull String... ops) {
1507         adoptShellPermissionIdentity(null);
1508         try {
1509             for (String op : ops) {
1510                 getContext().getSystemService(AppOpsManager.class).setUidMode(op, uid, mode);
1511             }
1512         } finally {
1513             dropShellPermissionIdentity();
1514         }
1515     }
1516 
1517     /**
1518      * Queries {@link ContentResolver} for a file IS_PENDING=0 and returns a {@link Cursor} with the
1519      * given columns.
1520      */
1521     @NonNull
queryFileExcludingPending(@onNull File file, String... projection)1522     public static Cursor queryFileExcludingPending(@NonNull File file, String... projection) {
1523         return queryFile(getContentResolver(), MediaStore.Files.getContentUri(sStorageVolumeName),
1524                 file, /*includePending*/ false, projection);
1525     }
1526 
1527     @NonNull
queryFile(ContentResolver cr, @NonNull File file, String... projection)1528     public static Cursor queryFile(ContentResolver cr, @NonNull File file, String... projection) {
1529         return queryFile(cr, MediaStore.Files.getContentUri(sStorageVolumeName),
1530                 file, /*includePending*/ true, projection);
1531     }
1532 
1533     @NonNull
queryFile(@onNull File file, String... projection)1534     public static Cursor queryFile(@NonNull File file, String... projection) {
1535         return queryFile(getContentResolver(), MediaStore.Files.getContentUri(sStorageVolumeName),
1536                 file, /*includePending*/ true, projection);
1537     }
1538 
1539     @NonNull
queryFile(ContentResolver cr, @NonNull Uri uri, @NonNull File file, boolean includePending, String... projection)1540     private static Cursor queryFile(ContentResolver cr, @NonNull Uri uri, @NonNull File file,
1541             boolean includePending, String... projection) {
1542         Bundle queryArgs = new Bundle();
1543         queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SELECTION,
1544                 MediaStore.MediaColumns.DATA + " = ?");
1545         queryArgs.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
1546                 new String[] { file.getAbsolutePath() });
1547         queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE);
1548 
1549         if (includePending) {
1550             queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_INCLUDE);
1551         } else {
1552             queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_EXCLUDE);
1553         }
1554 
1555         final Cursor c = cr.query(uri, projection, queryArgs, null);
1556         assertThat(c).isNotNull();
1557         return c;
1558     }
1559 
isObbDirUnmounted()1560     private static boolean isObbDirUnmounted() {
1561         List<String> mounts = new ArrayList<>();
1562         try {
1563             for (String line : executeShellCommand("cat /proc/mounts").split("\n")) {
1564                 String[] split = line.split(" ");
1565                 // Only check obb dirs with tmpfs, as if it's mounted for app data
1566                 // isolation, it will be tmpfs only.
1567                 if (split[0].equals("tmpfs") && split[1].startsWith("/storage/")
1568                         && split[1].endsWith("/obb")) {
1569                     return false;
1570                 }
1571             }
1572         } catch (IOException e) {
1573             Log.e(TAG, "Failed to execute shell command", e);
1574         }
1575         return true;
1576     }
1577 
1578     /**
1579      * Creates a new virtual public volume and returns the volume's name.
1580      */
createNewPublicVolume()1581     public static void createNewPublicVolume() throws Exception {
1582         // Unmount data and obb dirs for test app first so test app won't be killed during
1583         // volume unmount.
1584         executeShellCommand("sm unmount-app-data-dirs " + getContext().getPackageName() + " "
1585                     + android.os.Process.myPid() + " " + android.os.UserHandle.myUserId());
1586         pollForCondition(TestUtils::isObbDirUnmounted,
1587                 "Timed out while waiting for unmounting obb dir");
1588         executeShellCommand("sm set-force-adoptable on");
1589         executeShellCommand("sm set-virtual-disk true");
1590         Thread.sleep(2000);
1591         pollForCondition(TestUtils::partitionDisk, "Timed out while waiting for disk partitioning");
1592     }
1593 
partitionDisk()1594     private static boolean partitionDisk() {
1595         try {
1596             final String listDisks = executeShellCommand("sm list-disks").trim();
1597             if (TextUtils.isEmpty(listDisks)) {
1598                 return false;
1599             }
1600             executeShellCommand("sm partition " + listDisks + " public");
1601             return true;
1602         } catch (Exception e) {
1603             return false;
1604         }
1605     }
1606 
1607     /**
1608      * Gets the name of the public volume, waiting for a bit for it to be available.
1609      */
getPublicVolumeName()1610     public static String getPublicVolumeName() throws Exception {
1611         final String[] volName = new String[1];
1612         pollForCondition(() -> {
1613             volName[0] = getCurrentPublicVolumeName();
1614             return volName[0] != null;
1615         }, "Timed out while waiting for public volume to be ready");
1616 
1617         return volName[0];
1618     }
1619 
1620     /**
1621      * @return the currently mounted public volume, if any.
1622      */
getCurrentPublicVolumeName()1623     public static String getCurrentPublicVolumeName() {
1624         final String[] allVolumeDetails;
1625         try {
1626             allVolumeDetails = executeShellCommand("sm list-volumes")
1627                     .trim().split("\n");
1628         } catch (Exception e) {
1629             Log.e(TAG, "Failed to execute shell command", e);
1630             return null;
1631         }
1632         for (String volDetails : allVolumeDetails) {
1633             if (volDetails.startsWith("public")) {
1634                 final String[] publicVolumeDetails = volDetails.trim().split(" ");
1635                 String res = publicVolumeDetails[publicVolumeDetails.length - 1];
1636                 if ("null".equals(res)) {
1637                     continue;
1638                 }
1639                 return res;
1640             }
1641         }
1642         return null;
1643     }
1644 
1645     /**
1646      * Returns the content URI of the volume on which the test is running.
1647      */
getTestVolumeFileUri()1648     public static Uri getTestVolumeFileUri() {
1649         return MediaStore.Files.getContentUri(sStorageVolumeName);
1650     }
1651 
pollForCondition(Supplier<Boolean> condition, String errorMessage)1652     private static void pollForCondition(Supplier<Boolean> condition, String errorMessage)
1653             throws Exception {
1654         for (int i = 0; i < POLLING_TIMEOUT_MILLIS / POLLING_SLEEP_MILLIS; i++) {
1655             if (condition.get()) {
1656                 return;
1657             }
1658             Thread.sleep(POLLING_SLEEP_MILLIS);
1659         }
1660         throw new TimeoutException(errorMessage);
1661     }
1662 
1663     /**
1664      * Polls for all files access to be allowed.
1665      */
pollForManageExternalStorageAllowed()1666     public static void pollForManageExternalStorageAllowed() throws Exception {
1667         pollForCondition(
1668                 () -> Environment.isExternalStorageManager(),
1669                 "Timed out while waiting for MANAGE_EXTERNAL_STORAGE");
1670     }
1671 
assertVolumeType(boolean isPrimary)1672     private static void assertVolumeType(boolean isPrimary) {
1673         String[] parts = getExternalFilesDir().getAbsolutePath().split("/");
1674         assertThat(parts.length).isAtLeast(3);
1675         assertThat(parts[1]).isEqualTo("storage");
1676         if (isPrimary) {
1677             assertThat(parts[2]).isEqualTo("emulated");
1678         } else {
1679             assertThat(parts[2]).isNotEqualTo("emulated");
1680         }
1681     }
1682 
isProcessRunning(String packageName)1683     private static boolean isProcessRunning(String packageName) {
1684         return getAppProcessInfo(packageName).isPresent();
1685     }
1686 
getAppProcessInfo( String packageName)1687     private static Optional<ActivityManager.RunningAppProcessInfo> getAppProcessInfo(
1688             String packageName) {
1689         return getContext().getSystemService(
1690                 ActivityManager.class).getRunningAppProcesses().stream().filter(
1691                         p -> packageName.equals(p.processName)).findFirst();
1692     }
1693 }
1694