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