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