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