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("dd bs=1 if=%s skip=%d count=%d of=%s",
283                         source.getAbsolutePath(), skip, count, file.getAbsolutePath()));
284 
285                 // Force sync to try updating other views
286                 executeShellCommand("sync");
287             }
288         } else {
289             final File dir = file.getParentFile();
290             dir.mkdirs();
291             if (!dir.exists()) {
292                 throw new FileNotFoundException("Failed to create parent for " + file);
293             }
294             try (InputStream source = context.getResources().openRawResource(resId);
295                     OutputStream target = new FileOutputStream(file)) {
296                 FileUtils.copy(source, target);
297             }
298         }
299         return waitUntilExists(file);
300     }
301 
stageMedia(int resId, Uri collectionUri)302     public static Uri stageMedia(int resId, Uri collectionUri) throws IOException {
303         return stageMedia(resId, collectionUri, "image/png");
304     }
305 
stageMedia(int resId, Uri collectionUri, String mimeType)306     public static Uri stageMedia(int resId, Uri collectionUri, String mimeType) throws IOException {
307         final Context context = InstrumentationRegistry.getTargetContext();
308         final String displayName = "cts" + System.nanoTime();
309         final PendingParams params = new PendingParams(collectionUri, displayName, mimeType);
310         final Uri pendingUri = MediaStoreUtils.createPending(context, params);
311         try (PendingSession session = MediaStoreUtils.openPending(context, pendingUri)) {
312             try (InputStream source = context.getResources().openRawResource(resId);
313                     OutputStream target = session.openOutputStream()) {
314                 FileUtils.copy(source, target);
315             }
316             return session.publish();
317         }
318     }
319 
scanFile(File file)320     public static Uri scanFile(File file) throws Exception {
321         final Uri uri = MediaStore
322                 .scanFile(InstrumentationRegistry.getTargetContext().getContentResolver(), file);
323         assertWithMessage("no URI for '%s'", file).that(uri).isNotNull();
324         return uri;
325     }
326 
scanFileFromShell(File file)327     public static Uri scanFileFromShell(File file) throws Exception {
328         return scanFile(file);
329     }
330 
scanVolume(File file)331     public static void scanVolume(File file) throws Exception {
332         final StorageVolume vol = InstrumentationRegistry.getTargetContext()
333                 .getSystemService(StorageManager.class).getStorageVolume(file);
334         MediaStore.scanVolume(InstrumentationRegistry.getTargetContext().getContentResolver(),
335                 vol.getMediaStoreVolumeName());
336     }
337 
setOwner(Uri uri, String packageName)338     public static void setOwner(Uri uri, String packageName) throws Exception {
339         executeShellCommand("content update"
340                 + " --user " + InstrumentationRegistry.getTargetContext().getUserId()
341                 + " --uri " + uri
342                 + " --bind owner_package_name:s:" + packageName);
343     }
344 
clearOwner(Uri uri)345     public static void clearOwner(Uri uri) throws Exception {
346         executeShellCommand("content update"
347                 + " --user " + InstrumentationRegistry.getTargetContext().getUserId()
348                 + " --uri " + uri
349                 + " --bind owner_package_name:n:");
350     }
351 
hash(InputStream in)352     public static byte[] hash(InputStream in) throws Exception {
353         try (DigestInputStream digestIn = new DigestInputStream(in,
354                 MessageDigest.getInstance("SHA-1"));
355                 OutputStream out = new FileOutputStream(new File("/dev/null"))) {
356             FileUtils.copy(digestIn, out);
357             return digestIn.getMessageDigest().digest();
358         }
359     }
360 
361     /**
362      * Extract the average overall color of the given bitmap.
363      * <p>
364      * Internally takes advantage of gaussian blurring that is naturally applied
365      * when downscaling an image.
366      */
extractAverageColor(Bitmap bitmap)367     public static int extractAverageColor(Bitmap bitmap) {
368         final Bitmap res = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
369         final Canvas canvas = new Canvas(res);
370         final Rect src = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
371         final Rect dst = new Rect(0, 0, 1, 1);
372         canvas.drawBitmap(bitmap, src, dst, null);
373         return res.getPixel(0, 0);
374     }
375 
assertColorMostlyEquals(int expected, int actual)376     public static void assertColorMostlyEquals(int expected, int actual) {
377         assertTrue("Expected " + Integer.toHexString(expected) + " but was "
378                 + Integer.toHexString(actual), isColorMostlyEquals(expected, actual));
379     }
380 
assertColorMostlyNotEquals(int expected, int actual)381     public static void assertColorMostlyNotEquals(int expected, int actual) {
382         assertFalse("Expected " + Integer.toHexString(expected) + " but was "
383                 + Integer.toHexString(actual), isColorMostlyEquals(expected, actual));
384     }
385 
isColorMostlyEquals(int expected, int actual)386     private static boolean isColorMostlyEquals(int expected, int actual) {
387         final float[] expectedHSV = new float[3];
388         final float[] actualHSV = new float[3];
389         Color.colorToHSV(expected, expectedHSV);
390         Color.colorToHSV(actual, actualHSV);
391 
392         // Fail if more than a 10% difference in any component
393         if (Math.abs(expectedHSV[0] - actualHSV[0]) > 36) return false;
394         if (Math.abs(expectedHSV[1] - actualHSV[1]) > 0.1f) return false;
395         if (Math.abs(expectedHSV[2] - actualHSV[2]) > 0.1f) return false;
396         return true;
397     }
398 
assertExists(String path)399     public static void assertExists(String path) throws IOException {
400         assertExists(null, path);
401     }
402 
assertExists(File file)403     public static void assertExists(File file) throws IOException {
404         assertExists(null, file.getAbsolutePath());
405     }
406 
assertExists(String msg, String path)407     public static void assertExists(String msg, String path) throws IOException {
408         if (!access(path)) {
409             if (msg != null) {
410                 fail(path + ": " + msg);
411             } else {
412                 fail("File " + path + " does not exist");
413             }
414         }
415     }
416 
assertNotExists(String path)417     public static void assertNotExists(String path) throws IOException {
418         assertNotExists(null, path);
419     }
420 
assertNotExists(File file)421     public static void assertNotExists(File file) throws IOException {
422         assertNotExists(null, file.getAbsolutePath());
423     }
424 
assertNotExists(String msg, String path)425     public static void assertNotExists(String msg, String path) throws IOException {
426         if (access(path)) {
427             fail(msg);
428         }
429     }
430 
access(String path)431     private static boolean access(String path) throws IOException {
432         // The caller may be trying to stage into a location only available to
433         // the shell user, so we need to perform the entire copy as the shell
434         if (FileUtils.contains(Environment.getStorageDirectory(), new File(path))) {
435             return executeShellCommand("ls -la " + path).contains(path);
436         } else {
437             try {
438                 Os.access(path, OsConstants.F_OK);
439                 return true;
440             } catch (ErrnoException e) {
441                 if (e.errno == OsConstants.ENOENT) {
442                     return false;
443                 } else {
444                     throw new IOException(e.getMessage());
445                 }
446             }
447         }
448     }
449 
containsId(Uri uri, long id)450     public static boolean containsId(Uri uri, long id) {
451         return containsId(uri, null, id);
452     }
453 
containsId(Uri uri, Bundle extras, long id)454     public static boolean containsId(Uri uri, Bundle extras, long id) {
455         try (Cursor c = InstrumentationRegistry.getTargetContext().getContentResolver().query(uri,
456                 new String[] { MediaColumns._ID }, extras, null)) {
457             while (c.moveToNext()) {
458                 if (c.getLong(0) == id) return true;
459             }
460         }
461         return false;
462     }
463 
464     /**
465      * Gets File corresponding to the uri.
466      * This function assumes that the caller has access to the uri
467      * @param uri uri to get File for
468      * @return File file corresponding to the uri
469      * @throws FileNotFoundException if either the file does not exist or the caller does not have
470      * read access to the file
471      */
getRawFile(Uri uri)472     public static File getRawFile(Uri uri) throws Exception {
473         String filePath;
474         try (Cursor c = InstrumentationRegistry.getTargetContext().getContentResolver().query(uri,
475                 new String[] { MediaColumns.DATA }, null, null)) {
476             assertTrue(c.moveToFirst());
477             filePath = c.getString(0);
478         }
479         if (filePath != null) {
480             return new File(filePath);
481         } else {
482             throw new FileNotFoundException("Failed to find _data for " + uri);
483         }
484     }
485 
getRawFileHash(File file)486     public static String getRawFileHash(File file) throws Exception {
487         MessageDigest digest = MessageDigest.getInstance("SHA-1");
488         try (InputStream in = new BufferedInputStream(Files.newInputStream(file.toPath()))) {
489             byte[] buf = new byte[4096];
490             int n;
491             while ((n = in.read(buf)) >= 0) {
492                 digest.update(buf, 0, n);
493             }
494         }
495 
496         byte[] hash = digest.digest();
497         return BaseEncoding.base16().encode(hash);
498     }
499 
getRelativeFile(Uri uri)500     public static File getRelativeFile(Uri uri) throws Exception {
501         final String path = getRawFile(uri).getAbsolutePath();
502         final Matcher matcher = PATTERN_STORAGE_PATH.matcher(path);
503         if (matcher.find()) {
504             return new File(path.substring(matcher.end()));
505         } else {
506             throw new IllegalArgumentException();
507         }
508     }
509 
510     /** Revokes ACCESS_MEDIA_LOCATION from the test app */
revokeMediaLocationPermission(Context context)511     public static void revokeMediaLocationPermission(Context context) throws Exception {
512         try {
513             InstrumentationRegistry.getInstrumentation().getUiAutomation()
514                     .adoptShellPermissionIdentity("android.permission.MANAGE_APP_OPS_MODES",
515                             "android.permission.REVOKE_RUNTIME_PERMISSIONS");
516 
517             // Revoking ACCESS_MEDIA_LOCATION permission will kill the test app.
518             // Deny access_media_permission App op to revoke this permission.
519             PackageManager packageManager = context.getPackageManager();
520             String packageName = context.getPackageName();
521             if (packageManager.checkPermission(android.Manifest.permission.ACCESS_MEDIA_LOCATION,
522                     packageName) == PackageManager.PERMISSION_GRANTED) {
523                 context.getPackageManager().updatePermissionFlags(
524                         android.Manifest.permission.ACCESS_MEDIA_LOCATION, packageName,
525                         PackageManager.FLAG_PERMISSION_REVOKED_COMPAT,
526                         PackageManager.FLAG_PERMISSION_REVOKED_COMPAT, context.getUser());
527                 context.getSystemService(AppOpsManager.class).setUidMode(
528                         "android:access_media_location", Process.myUid(),
529                         AppOpsManager.MODE_IGNORED);
530             }
531         } finally {
532             InstrumentationRegistry.getInstrumentation().getUiAutomation().
533                     dropShellPermissionIdentity();
534         }
535     }
536 }
537