• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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.provider.cts;
18 
19 import static com.google.common.truth.Truth.assertWithMessage;
20 
21 import static org.junit.Assert.assertFalse;
22 import static org.junit.Assert.assertTrue;
23 import static org.junit.Assert.fail;
24 
25 import android.app.AppOpsManager;
26 import android.app.UiAutomation;
27 import android.content.Context;
28 import android.content.pm.PackageManager;
29 import android.content.res.AssetFileDescriptor;
30 import android.database.Cursor;
31 import android.graphics.Bitmap;
32 import android.graphics.Canvas;
33 import android.graphics.Color;
34 import android.graphics.Rect;
35 import android.net.Uri;
36 import android.os.Bundle;
37 import android.os.Environment;
38 import android.os.FileUtils;
39 import android.os.ParcelFileDescriptor;
40 import android.os.Process;
41 import android.os.UserManager;
42 import android.os.storage.StorageManager;
43 import android.os.storage.StorageVolume;
44 import android.provider.MediaStore;
45 import android.provider.MediaStore.MediaColumns;
46 import android.provider.cts.media.MediaStoreUtils;
47 import android.provider.cts.media.MediaStoreUtils.PendingParams;
48 import android.provider.cts.media.MediaStoreUtils.PendingSession;
49 import android.system.ErrnoException;
50 import android.system.Os;
51 import android.system.OsConstants;
52 import android.util.Log;
53 
54 import androidx.test.InstrumentationRegistry;
55 
56 import com.android.compatibility.common.util.Timeout;
57 
58 import com.google.common.io.BaseEncoding;
59 
60 import java.io.BufferedInputStream;
61 import java.io.BufferedReader;
62 import java.io.File;
63 import java.io.FileInputStream;
64 import java.io.FileNotFoundException;
65 import java.io.FileOutputStream;
66 import java.io.IOException;
67 import java.io.InputStream;
68 import java.io.InputStreamReader;
69 import java.io.OutputStream;
70 import java.nio.charset.StandardCharsets;
71 import java.nio.file.Files;
72 import java.security.DigestInputStream;
73 import java.security.MessageDigest;
74 import java.util.HashSet;
75 import java.util.Objects;
76 import java.util.Set;
77 import java.util.regex.Matcher;
78 import java.util.regex.Pattern;
79 
80 /**
81  * Utility methods for provider cts tests.
82  */
83 public class ProviderTestUtils {
84     static final String TAG = "ProviderTestUtils";
85 
86     private static final int BACKUP_TIMEOUT_MILLIS = 4000;
87     private static final Pattern BMGR_ENABLED_PATTERN = Pattern.compile(
88             "^Backup Manager currently (enabled|disabled)$");
89 
90     private static final Pattern PATTERN_STORAGE_PATH = Pattern.compile(
91             "(?i)^/storage/[^/]+/(?:[0-9]+/)?");
92 
93     private static final Timeout IO_TIMEOUT = new Timeout("IO_TIMEOUT", 2_000, 2, 2_000);
94 
getSharedVolumeNames()95     public static Iterable<String> getSharedVolumeNames() {
96         // We test both new and legacy volume names
97         final HashSet<String> testVolumes = new HashSet<>();
98         final Set<String> volumeNames = MediaStore.getExternalVolumeNames(
99                 InstrumentationRegistry.getTargetContext());
100         // Run tests only on VISIBLE volumes which are FUSE mounted and indexed by MediaProvider
101         for (String vol : volumeNames) {
102             final File mountedPath = getVolumePath(vol);
103             if (mountedPath == null || mountedPath.getAbsolutePath() == null) continue;
104             if (mountedPath.getAbsolutePath().startsWith("/storage/")) {
105                 testVolumes.add(vol);
106             }
107         }
108         testVolumes.add(MediaStore.VOLUME_EXTERNAL);
109         return testVolumes;
110     }
111 
resolveVolumeName(String volumeName)112     public static String resolveVolumeName(String volumeName) {
113         if (MediaStore.VOLUME_EXTERNAL.equals(volumeName)) {
114             return MediaStore.VOLUME_EXTERNAL_PRIMARY;
115         } else {
116             return volumeName;
117         }
118     }
119 
setDefaultSmsApp(boolean setToSmsApp, String packageName, UiAutomation uiAutomation)120     static void setDefaultSmsApp(boolean setToSmsApp, String packageName, UiAutomation uiAutomation)
121             throws Exception {
122         String mode = setToSmsApp ? "allow" : "default";
123         String cmd = "appops set %s %s %s";
124         executeShellCommand(String.format(cmd, packageName, "WRITE_SMS", mode), uiAutomation);
125         executeShellCommand(String.format(cmd, packageName, "READ_SMS", mode), uiAutomation);
126     }
127 
executeShellCommand(String command)128     public static String executeShellCommand(String command) throws IOException {
129         return executeShellCommand(command,
130                 InstrumentationRegistry.getInstrumentation().getUiAutomation());
131     }
132 
executeShellCommand(String command, UiAutomation uiAutomation)133     public static String executeShellCommand(String command, UiAutomation uiAutomation)
134             throws IOException {
135         Log.v(TAG, "$ " + command);
136         ParcelFileDescriptor pfd = uiAutomation.executeShellCommand(command.toString());
137         BufferedReader br = null;
138         try (InputStream in = new FileInputStream(pfd.getFileDescriptor());) {
139             br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
140             String str = null;
141             StringBuilder out = new StringBuilder();
142             while ((str = br.readLine()) != null) {
143                 Log.v(TAG, "> " + str);
144                 out.append(str);
145             }
146             return out.toString();
147         } finally {
148             if (br != null) {
149                 br.close();
150             }
151         }
152     }
153 
setBackupTransport(String transport, UiAutomation uiAutomation)154     static String setBackupTransport(String transport, UiAutomation uiAutomation) throws Exception {
155         String output = executeShellCommand("bmgr transport " + transport, uiAutomation);
156         Pattern pattern = Pattern.compile("\\(formerly (.*)\\)$");
157         Matcher matcher = pattern.matcher(output);
158         if (matcher.find()) {
159             return matcher.group(1);
160         } else {
161             throw new Exception("non-parsable output setting bmgr transport: " + output);
162         }
163     }
164 
setBackupEnabled(boolean enable, UiAutomation uiAutomation)165     static boolean setBackupEnabled(boolean enable, UiAutomation uiAutomation) throws Exception {
166         // Check to see the previous state of the backup service
167         boolean previouslyEnabled = false;
168         String output = executeShellCommand("bmgr enabled", uiAutomation);
169         Matcher matcher = BMGR_ENABLED_PATTERN.matcher(output.trim());
170         if (matcher.find()) {
171             previouslyEnabled = "enabled".equals(matcher.group(1));
172         } else {
173             throw new RuntimeException("Backup output format changed.  No longer matches"
174                     + " expected regex: " + BMGR_ENABLED_PATTERN + "\nactual: '" + output + "'");
175         }
176 
177         executeShellCommand("bmgr enable " + enable, uiAutomation);
178         return previouslyEnabled;
179     }
180 
hasBackupTransport(String transport, UiAutomation uiAutomation)181     static boolean hasBackupTransport(String transport, UiAutomation uiAutomation)
182             throws Exception {
183         String output = executeShellCommand("bmgr list transports", uiAutomation);
184         for (String t : output.split(" ")) {
185             if ("*".equals(t)) {
186                 // skip the current selection marker.
187                 continue;
188             } else if (Objects.equals(transport, t)) {
189                 return true;
190             }
191         }
192         return false;
193     }
194 
runBackup(String packageName, UiAutomation uiAutomation)195     static void runBackup(String packageName, UiAutomation uiAutomation) throws Exception {
196         executeShellCommand("bmgr backupnow " + packageName, uiAutomation);
197         Thread.sleep(BACKUP_TIMEOUT_MILLIS);
198     }
199 
runRestore(String packageName, UiAutomation uiAutomation)200     static void runRestore(String packageName, UiAutomation uiAutomation) throws Exception {
201         executeShellCommand("bmgr restore 1 " + packageName, uiAutomation);
202         Thread.sleep(BACKUP_TIMEOUT_MILLIS);
203     }
204 
wipeBackup(String backupTransport, String packageName, UiAutomation uiAutomation)205     static void wipeBackup(String backupTransport, String packageName, UiAutomation uiAutomation)
206             throws Exception {
207         executeShellCommand("bmgr wipe " + backupTransport + " " + packageName, uiAutomation);
208     }
209 
waitForIdle()210     public static void waitForIdle() {
211         MediaStore.waitForIdle(InstrumentationRegistry.getTargetContext().getContentResolver());
212     }
213 
214     /**
215      * Waits until a file exists, or fails.
216      *
217      * @return existing file.
218      */
waitUntilExists(File file)219     public static File waitUntilExists(File file) throws IOException {
220         try {
221             return IO_TIMEOUT.run("file '" + file + "' doesn't exist yet", () -> {
222                 return file.exists() ? file : null; // will retry if it returns null
223             });
224         } catch (Exception e) {
225             throw new IOException(e);
226         }
227     }
228 
getVolumePath(String volumeName)229     public static File getVolumePath(String volumeName) {
230         final Context context = InstrumentationRegistry.getTargetContext();
231         return context.getSystemService(StorageManager.class)
232                 .getStorageVolume(MediaStore.Files.getContentUri(volumeName)).getDirectory();
233     }
234 
stageDir(String volumeName)235     public static File stageDir(String volumeName) throws IOException {
236         if (MediaStore.VOLUME_EXTERNAL.equals(volumeName)) {
237             volumeName = MediaStore.VOLUME_EXTERNAL_PRIMARY;
238         }
239         final StorageVolume vol = InstrumentationRegistry.getTargetContext()
240                 .getSystemService(StorageManager.class)
241                 .getStorageVolume(MediaStore.Files.getContentUri(volumeName));
242         File dir = Environment.buildPath(vol.getDirectory(), "Android", "media",
243                 "android.provider.cts");
244         Log.d(TAG, "stageDir(" + volumeName + "): returning " + dir);
245         return dir;
246     }
247 
stageDownloadDir(String volumeName)248     public static File stageDownloadDir(String volumeName) throws IOException {
249         if (MediaStore.VOLUME_EXTERNAL.equals(volumeName)) {
250             volumeName = MediaStore.VOLUME_EXTERNAL_PRIMARY;
251         }
252         final StorageVolume vol = InstrumentationRegistry.getTargetContext()
253                 .getSystemService(StorageManager.class)
254                 .getStorageVolume(MediaStore.Files.getContentUri(volumeName));
255         return Environment.buildPath(vol.getDirectory(),
256                 Environment.DIRECTORY_DOWNLOADS, "android.provider.cts");
257     }
258 
stageFile(int resId, File file)259     public static File stageFile(int resId, File file) throws IOException {
260         // The caller may be trying to stage into a location only available to
261         // the shell user, so we need to perform the entire copy as the shell
262         final Context context = InstrumentationRegistry.getTargetContext();
263         UserManager userManager = context.getSystemService(UserManager.class);
264         if (userManager.isSystemUser() &&
265                     FileUtils.contains(Environment.getStorageDirectory(), file)) {
266             executeShellCommand("mkdir -p " + file.getParent());
267             waitUntilExists(file.getParentFile());
268             try (AssetFileDescriptor afd = context.getResources().openRawResourceFd(resId)) {
269                 final File source = ParcelFileDescriptor.getFile(afd.getFileDescriptor());
270                 final long skip = afd.getStartOffset();
271                 final long count = afd.getLength();
272 
273                 try {
274                     // Try to create the file as calling package so that calling package remains
275                     // as owner of the file.
276                     file.createNewFile();
277                 } catch (IOException ignored) {
278                     // Apps can't create files in other app's private directories, but shell can. If
279                     // file creation fails, we ignore and let `dd` command create it instead.
280                 }
281 
282                 executeShellCommand(String.format(
283                         "dd bs=4K if=%s iflag=skip_bytes,count_bytes skip=%d count=%d of=%s",
284                         source.getAbsolutePath(), skip, count, file.getAbsolutePath()));
285 
286                 // Force sync to try updating other views
287                 executeShellCommand("sync");
288             }
289         } else {
290             final File dir = file.getParentFile();
291             dir.mkdirs();
292             if (!dir.exists()) {
293                 throw new FileNotFoundException("Failed to create parent for " + file);
294             }
295             try (InputStream source = context.getResources().openRawResource(resId);
296                     OutputStream target = new FileOutputStream(file)) {
297                 FileUtils.copy(source, target);
298             }
299         }
300         return waitUntilExists(file);
301     }
302 
stageMedia(int resId, Uri collectionUri)303     public static Uri stageMedia(int resId, Uri collectionUri) throws IOException {
304         return stageMedia(resId, collectionUri, "image/png");
305     }
306 
stageMedia(int resId, Uri collectionUri, String mimeType)307     public static Uri stageMedia(int resId, Uri collectionUri, String mimeType) throws IOException {
308         final Context context = InstrumentationRegistry.getTargetContext();
309         final String displayName = "cts" + System.nanoTime();
310         final PendingParams params = new PendingParams(collectionUri, displayName, mimeType);
311         final Uri pendingUri = MediaStoreUtils.createPending(context, params);
312         try (PendingSession session = MediaStoreUtils.openPending(context, pendingUri)) {
313             try (InputStream source = context.getResources().openRawResource(resId);
314                     OutputStream target = session.openOutputStream()) {
315                 FileUtils.copy(source, target);
316             }
317             return session.publish();
318         }
319     }
320 
scanFile(File file)321     public static Uri scanFile(File file) throws Exception {
322         final Uri uri = MediaStore
323                 .scanFile(InstrumentationRegistry.getTargetContext().getContentResolver(), file);
324         assertWithMessage("no URI for '%s'", file).that(uri).isNotNull();
325         return uri;
326     }
327 
scanFileFromShell(File file)328     public static Uri scanFileFromShell(File file) throws Exception {
329         return scanFile(file);
330     }
331 
scanVolume(File file)332     public static void scanVolume(File file) throws Exception {
333         final StorageVolume vol = InstrumentationRegistry.getTargetContext()
334                 .getSystemService(StorageManager.class).getStorageVolume(file);
335         MediaStore.scanVolume(InstrumentationRegistry.getTargetContext().getContentResolver(),
336                 vol.getMediaStoreVolumeName());
337     }
338 
setOwner(Uri uri, String packageName)339     public static void setOwner(Uri uri, String packageName) throws Exception {
340         executeShellCommand("content update"
341                 + " --user " + InstrumentationRegistry.getTargetContext().getUserId()
342                 + " --uri " + uri
343                 + " --bind owner_package_name:s:" + packageName);
344     }
345 
clearOwner(Uri uri)346     public static void clearOwner(Uri uri) throws Exception {
347         executeShellCommand("content update"
348                 + " --user " + InstrumentationRegistry.getTargetContext().getUserId()
349                 + " --uri " + uri
350                 + " --bind owner_package_name:n:");
351     }
352 
hash(InputStream in)353     public static byte[] hash(InputStream in) throws Exception {
354         try (DigestInputStream digestIn = new DigestInputStream(in,
355                 MessageDigest.getInstance("SHA-1"));
356                 OutputStream out = new FileOutputStream(new File("/dev/null"))) {
357             FileUtils.copy(digestIn, out);
358             return digestIn.getMessageDigest().digest();
359         }
360     }
361 
362     /**
363      * Extract the average overall color of the given bitmap.
364      * <p>
365      * Internally takes advantage of gaussian blurring that is naturally applied
366      * when downscaling an image.
367      */
extractAverageColor(Bitmap bitmap)368     public static int extractAverageColor(Bitmap bitmap) {
369         final Bitmap res = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
370         final Canvas canvas = new Canvas(res);
371         final Rect src = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
372         final Rect dst = new Rect(0, 0, 1, 1);
373         canvas.drawBitmap(bitmap, src, dst, null);
374         return res.getPixel(0, 0);
375     }
376 
assertColorMostlyEquals(int expected, int actual)377     public static void assertColorMostlyEquals(int expected, int actual) {
378         assertTrue("Expected " + Integer.toHexString(expected) + " but was "
379                 + Integer.toHexString(actual), isColorMostlyEquals(expected, actual));
380     }
381 
assertColorMostlyNotEquals(int expected, int actual)382     public static void assertColorMostlyNotEquals(int expected, int actual) {
383         assertFalse("Expected " + Integer.toHexString(expected) + " but was "
384                 + Integer.toHexString(actual), isColorMostlyEquals(expected, actual));
385     }
386 
isColorMostlyEquals(int expected, int actual)387     private static boolean isColorMostlyEquals(int expected, int actual) {
388         final float[] expectedHSV = new float[3];
389         final float[] actualHSV = new float[3];
390         Color.colorToHSV(expected, expectedHSV);
391         Color.colorToHSV(actual, actualHSV);
392 
393         // Fail if more than a 10% difference in any component
394         if (Math.abs(expectedHSV[0] - actualHSV[0]) > 36) return false;
395         if (Math.abs(expectedHSV[1] - actualHSV[1]) > 0.1f) return false;
396         if (Math.abs(expectedHSV[2] - actualHSV[2]) > 0.1f) return false;
397         return true;
398     }
399 
assertExists(String path)400     public static void assertExists(String path) throws IOException {
401         assertExists(null, path);
402     }
403 
assertExists(File file)404     public static void assertExists(File file) throws IOException {
405         assertExists(null, file.getAbsolutePath());
406     }
407 
assertExists(String msg, String path)408     public static void assertExists(String msg, String path) throws IOException {
409         if (!access(path)) {
410             if (msg != null) {
411                 fail(path + ": " + msg);
412             } else {
413                 fail("File " + path + " does not exist");
414             }
415         }
416     }
417 
assertNotExists(String path)418     public static void assertNotExists(String path) throws IOException {
419         assertNotExists(null, path);
420     }
421 
assertNotExists(File file)422     public static void assertNotExists(File file) throws IOException {
423         assertNotExists(null, file.getAbsolutePath());
424     }
425 
assertNotExists(String msg, String path)426     public static void assertNotExists(String msg, String path) throws IOException {
427         if (access(path)) {
428             fail(msg);
429         }
430     }
431 
access(String path)432     private static boolean access(String path) throws IOException {
433         // The caller may be trying to stage into a location only available to
434         // the shell user, so we need to perform the entire copy as the shell
435         if (FileUtils.contains(Environment.getStorageDirectory(), new File(path))) {
436             return executeShellCommand("ls -la " + path).contains(path);
437         } else {
438             try {
439                 Os.access(path, OsConstants.F_OK);
440                 return true;
441             } catch (ErrnoException e) {
442                 if (e.errno == OsConstants.ENOENT) {
443                     return false;
444                 } else {
445                     throw new IOException(e.getMessage());
446                 }
447             }
448         }
449     }
450 
containsId(Uri uri, long id)451     public static boolean containsId(Uri uri, long id) {
452         return containsId(uri, null, id);
453     }
454 
containsId(Uri uri, Bundle extras, long id)455     public static boolean containsId(Uri uri, Bundle extras, long id) {
456         try (Cursor c = InstrumentationRegistry.getTargetContext().getContentResolver().query(uri,
457                 new String[] { MediaColumns._ID }, extras, null)) {
458             while (c.moveToNext()) {
459                 if (c.getLong(0) == id) return true;
460             }
461         }
462         return false;
463     }
464 
465     /**
466      * Gets File corresponding to the uri.
467      * This function assumes that the caller has access to the uri
468      * @param uri uri to get File for
469      * @return File file corresponding to the uri
470      * @throws FileNotFoundException if either the file does not exist or the caller does not have
471      * read access to the file
472      */
getRawFile(Uri uri)473     public static File getRawFile(Uri uri) throws Exception {
474         String filePath;
475         try (Cursor c = InstrumentationRegistry.getTargetContext().getContentResolver().query(uri,
476                 new String[] { MediaColumns.DATA }, null, null)) {
477             assertTrue(c.moveToFirst());
478             filePath = c.getString(0);
479         }
480         if (filePath != null) {
481             return new File(filePath);
482         } else {
483             throw new FileNotFoundException("Failed to find _data for " + uri);
484         }
485     }
486 
getRawFileHash(File file)487     public static String getRawFileHash(File file) throws Exception {
488         MessageDigest digest = MessageDigest.getInstance("SHA-1");
489         try (InputStream in = new BufferedInputStream(Files.newInputStream(file.toPath()))) {
490             byte[] buf = new byte[4096];
491             int n;
492             while ((n = in.read(buf)) >= 0) {
493                 digest.update(buf, 0, n);
494             }
495         }
496 
497         byte[] hash = digest.digest();
498         return BaseEncoding.base16().encode(hash);
499     }
500 
getRelativeFile(Uri uri)501     public static File getRelativeFile(Uri uri) throws Exception {
502         final String path = getRawFile(uri).getAbsolutePath();
503         final Matcher matcher = PATTERN_STORAGE_PATH.matcher(path);
504         if (matcher.find()) {
505             return new File(path.substring(matcher.end()));
506         } else {
507             throw new IllegalArgumentException();
508         }
509     }
510 
511     /** Revokes ACCESS_MEDIA_LOCATION from the test app */
revokeMediaLocationPermission(Context context)512     public static void revokeMediaLocationPermission(Context context) throws Exception {
513         try {
514             InstrumentationRegistry.getInstrumentation().getUiAutomation()
515                     .adoptShellPermissionIdentity("android.permission.MANAGE_APP_OPS_MODES",
516                             "android.permission.REVOKE_RUNTIME_PERMISSIONS");
517 
518             // Revoking ACCESS_MEDIA_LOCATION permission will kill the test app.
519             // Deny access_media_permission App op to revoke this permission.
520             PackageManager packageManager = context.getPackageManager();
521             String packageName = context.getPackageName();
522             if (packageManager.checkPermission(android.Manifest.permission.ACCESS_MEDIA_LOCATION,
523                     packageName) == PackageManager.PERMISSION_GRANTED) {
524                 context.getPackageManager().updatePermissionFlags(
525                         android.Manifest.permission.ACCESS_MEDIA_LOCATION, packageName,
526                         PackageManager.FLAG_PERMISSION_REVOKED_COMPAT,
527                         PackageManager.FLAG_PERMISSION_REVOKED_COMPAT, context.getUser());
528                 context.getSystemService(AppOpsManager.class).setUidMode(
529                         "android:access_media_location", Process.myUid(),
530                         AppOpsManager.MODE_IGNORED);
531             }
532         } finally {
533             InstrumentationRegistry.getInstrumentation().getUiAutomation().
534                     dropShellPermissionIdentity();
535         }
536     }
537 }
538