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.mediaprovidertranscode.cts; 18 19 import static android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG; 20 import static android.mediaprovidertranscode.cts.TranscodeTestConstants.INTENT_EXTRA_CALLING_PKG; 21 import static android.mediaprovidertranscode.cts.TranscodeTestConstants.INTENT_EXTRA_PATH; 22 import static android.mediaprovidertranscode.cts.TranscodeTestConstants.INTENT_QUERY_TYPE; 23 import static android.provider.DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT; 24 25 import static androidx.test.InstrumentationRegistry.getContext; 26 27 import static com.google.common.truth.Truth.assertThat; 28 29 import static org.junit.Assert.assertEquals; 30 import static org.junit.Assert.assertTrue; 31 32 import android.Manifest; 33 import android.app.ActivityManager; 34 import android.app.AppOpsManager; 35 import android.app.UiAutomation; 36 import android.content.BroadcastReceiver; 37 import android.content.ContentResolver; 38 import android.content.Context; 39 import android.content.Intent; 40 import android.content.IntentFilter; 41 import android.content.pm.PackageManager; 42 import android.media.MediaCodecInfo; 43 import android.media.MediaCodecInfo.CodecCapabilities; 44 import android.media.MediaCodecList; 45 import android.media.MediaFormat; 46 import android.net.Uri; 47 import android.os.Bundle; 48 import android.os.Environment; 49 import android.os.FileUtils; 50 import android.os.ParcelFileDescriptor; 51 import android.os.Process; 52 import android.os.SystemClock; 53 import android.os.storage.StorageManager; 54 import android.provider.DeviceConfig; 55 import android.provider.MediaStore; 56 import android.system.Os; 57 import android.system.OsConstants; 58 import android.util.Log; 59 60 import androidx.annotation.NonNull; 61 import androidx.test.InstrumentationRegistry; 62 63 import com.android.cts.install.lib.Install; 64 import com.android.cts.install.lib.InstallUtils; 65 import com.android.cts.install.lib.TestApp; 66 import com.android.cts.install.lib.Uninstall; 67 import com.android.modules.utils.build.SdkLevel; 68 69 import com.google.common.io.ByteStreams; 70 71 import java.io.File; 72 import java.io.FileInputStream; 73 import java.io.FileOutputStream; 74 import java.io.IOException; 75 import java.io.InputStream; 76 import java.io.InterruptedIOException; 77 import java.util.Arrays; 78 import java.util.UUID; 79 import java.util.concurrent.CountDownLatch; 80 import java.util.concurrent.TimeUnit; 81 import java.util.concurrent.TimeoutException; 82 import java.util.function.Supplier; 83 84 public class TranscodeTestUtils { 85 private static final String TAG = "TranscodeTestUtils"; 86 87 private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(20); 88 private static final long POLLING_SLEEP_MILLIS = 100; 89 private static final String TRANSCODE_COMPAT_MANIFEST_DEVICE_CONFIG_PROPERTY_NAME = 90 "transcode_compat_manifest"; 91 stageHEVCVideoFile(File videoFile)92 public static Uri stageHEVCVideoFile(File videoFile) throws IOException { 93 return stageVideoFile(videoFile, R.raw.testvideo_HEVC); 94 } 95 stageSmallHevcVideoFile(File videoFile)96 public static Uri stageSmallHevcVideoFile(File videoFile) throws IOException { 97 return stageVideoFile(videoFile, R.raw.testVideo_HEVC_small); 98 } 99 stageMediumHevcVideoFile(File videoFile)100 public static Uri stageMediumHevcVideoFile(File videoFile) throws IOException { 101 return stageVideoFile(videoFile, R.raw.testVideo_HEVC_medium); 102 } 103 stageLongHevcVideoFile(File videoFile)104 public static Uri stageLongHevcVideoFile(File videoFile) throws IOException { 105 return stageVideoFile(videoFile, R.raw.testVideo_HEVC_long); 106 } 107 stageLegacyVideoFile(File videoFile)108 public static Uri stageLegacyVideoFile(File videoFile) throws IOException { 109 return stageVideoFile(videoFile, R.raw.testVideo_Legacy); 110 } 111 stageVideoFile(File videoFile, int resourceId)112 private static Uri stageVideoFile(File videoFile, int resourceId) throws IOException { 113 if (!videoFile.getParentFile().exists()) { 114 assertTrue(videoFile.getParentFile().mkdirs()); 115 } 116 try (InputStream in = 117 getContext().getResources().openRawResource(resourceId); 118 FileOutputStream out = new FileOutputStream(videoFile)) { 119 FileUtils.copy(in, out); 120 // Sync file to disk to ensure file is fully written to the lower fs before scanning 121 // Otherwise, media provider might try to read the file on the lower fs and not see 122 // the fully written bytes 123 out.getFD().sync(); 124 } 125 return MediaStore.scanFile(getContext().getContentResolver(), videoFile); 126 } 127 open(File file, boolean forWrite)128 public static ParcelFileDescriptor open(File file, boolean forWrite) throws Exception { 129 return ParcelFileDescriptor.open(file, forWrite ? ParcelFileDescriptor.MODE_READ_WRITE 130 : ParcelFileDescriptor.MODE_READ_ONLY); 131 } 132 open(Uri uri, boolean forWrite, Bundle bundle)133 public static ParcelFileDescriptor open(Uri uri, boolean forWrite, Bundle bundle) 134 throws Exception { 135 ContentResolver resolver = getContext().getContentResolver(); 136 if (bundle == null) { 137 return resolver.openFileDescriptor(uri, forWrite ? "rw" : "r"); 138 } else { 139 return resolver.openTypedAssetFileDescriptor(uri, "*/*", bundle) 140 .getParcelFileDescriptor(); 141 } 142 } 143 read(ParcelFileDescriptor parcelFileDescriptor, int byteCount, int fileOffset)144 static byte[] read(ParcelFileDescriptor parcelFileDescriptor, int byteCount, int fileOffset) 145 throws Exception { 146 assertThat(byteCount).isGreaterThan(-1); 147 assertThat(fileOffset).isGreaterThan(-1); 148 149 Os.lseek(parcelFileDescriptor.getFileDescriptor(), fileOffset, OsConstants.SEEK_SET); 150 151 byte[] bytes = new byte[byteCount]; 152 int numBytesRead = Os.read(parcelFileDescriptor.getFileDescriptor(), bytes, 153 0 /* byteOffset */, byteCount); 154 assertThat(numBytesRead).isGreaterThan(-1); 155 return bytes; 156 } 157 write(ParcelFileDescriptor parcelFileDescriptor, byte[] bytes, int byteCount, int fileOffset)158 static void write(ParcelFileDescriptor parcelFileDescriptor, byte[] bytes, int byteCount, 159 int fileOffset) throws Exception { 160 assertThat(byteCount).isGreaterThan(-1); 161 assertThat(fileOffset).isGreaterThan(-1); 162 163 Os.lseek(parcelFileDescriptor.getFileDescriptor(), fileOffset, OsConstants.SEEK_SET); 164 165 int numBytesWritten = Os.write(parcelFileDescriptor.getFileDescriptor(), bytes, 166 0 /* byteOffset */, byteCount); 167 assertThat(numBytesWritten).isNotEqualTo(-1); 168 assertThat(numBytesWritten).isEqualTo(byteCount); 169 } 170 enableTranscodingForPackage(String packageName)171 public static void enableTranscodingForPackage(String packageName) throws Exception { 172 if (SdkLevel.isAtLeastU()) { 173 getUiAutomation().adoptShellPermissionIdentity(WRITE_ALLOWLISTED_DEVICE_CONFIG); 174 try { 175 final String newPropertyValue = packageName + ",0"; 176 DeviceConfig.setProperty(NAMESPACE_STORAGE_NATIVE_BOOT, 177 TRANSCODE_COMPAT_MANIFEST_DEVICE_CONFIG_PROPERTY_NAME, newPropertyValue, 178 /* makeDefault */ false); 179 } finally { 180 getUiAutomation().dropShellPermissionIdentity(); 181 } 182 } else { 183 executeShellCommand("device_config put storage_native_boot transcode_compat_manifest " 184 + packageName + ",0"); 185 } 186 SystemClock.sleep(1000); 187 } 188 forceEnableAppCompatHevc(String packageName)189 public static void forceEnableAppCompatHevc(String packageName) throws IOException { 190 final String command = "am compat enable 174228127 " + packageName; 191 executeShellCommand(command); 192 } 193 forceDisableAppCompatHevc(String packageName)194 public static void forceDisableAppCompatHevc(String packageName) throws IOException { 195 final String command = "am compat enable 174227820 " + packageName; 196 executeShellCommand(command); 197 } 198 resetAppCompat(String packageName)199 public static void resetAppCompat(String packageName) throws IOException { 200 final String command = "am compat reset-all " + packageName; 201 executeShellCommand(command); 202 } 203 disableTranscodingForAllPackages()204 public static void disableTranscodingForAllPackages() throws Exception { 205 if (SdkLevel.isAtLeastU()) { 206 getUiAutomation().adoptShellPermissionIdentity(WRITE_ALLOWLISTED_DEVICE_CONFIG); 207 try { 208 DeviceConfig.deleteProperty(NAMESPACE_STORAGE_NATIVE_BOOT, 209 TRANSCODE_COMPAT_MANIFEST_DEVICE_CONFIG_PROPERTY_NAME); 210 } finally { 211 getUiAutomation().dropShellPermissionIdentity(); 212 } 213 } else { 214 executeShellCommand("device_config delete storage_native_boot " 215 + "transcode_compat_manifest"); 216 } 217 SystemClock.sleep(1000); 218 } 219 220 /** 221 * Executes a shell command. 222 */ executeShellCommand(String command)223 public static String executeShellCommand(String command) throws IOException { 224 int attempt = 0; 225 while (attempt++ < 5) { 226 try { 227 return executeShellCommandInternal(command); 228 } catch (InterruptedIOException e) { 229 // Hmm, we had trouble executing the shell command; the best we 230 // can do is try again a few more times 231 Log.v(TAG, "Trouble executing " + command + "; trying again", e); 232 } 233 } 234 throw new IOException("Failed to execute " + command); 235 } 236 executeShellCommandInternal(String cmd)237 private static String executeShellCommandInternal(String cmd) throws IOException { 238 UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 239 try (FileInputStream output = new FileInputStream( 240 uiAutomation.executeShellCommand(cmd).getFileDescriptor())) { 241 return new String(ByteStreams.toByteArray(output)); 242 } 243 } 244 245 /** 246 * Polls for external storage to be mounted. 247 */ pollForExternalStorageState()248 public static void pollForExternalStorageState() throws Exception { 249 pollForCondition( 250 () -> Environment.getExternalStorageState(Environment.getExternalStorageDirectory()) 251 .equals(Environment.MEDIA_MOUNTED), 252 "Timed out while waiting for ExternalStorageState to be MEDIA_MOUNTED"); 253 } 254 pollForCondition(Supplier<Boolean> condition, String errorMessage)255 private static void pollForCondition(Supplier<Boolean> condition, String errorMessage) 256 throws Exception { 257 for (int i = 0; i < POLLING_TIMEOUT_MILLIS / POLLING_SLEEP_MILLIS; i++) { 258 if (condition.get()) { 259 return; 260 } 261 Thread.sleep(POLLING_SLEEP_MILLIS); 262 } 263 throw new TimeoutException(errorMessage); 264 } 265 grantPermission(String packageName, String permission)266 public static void grantPermission(String packageName, String permission) { 267 UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 268 uiAutomation.adoptShellPermissionIdentity("android.permission.GRANT_RUNTIME_PERMISSIONS"); 269 try { 270 uiAutomation.grantRuntimePermission(packageName, permission); 271 } finally { 272 uiAutomation.dropShellPermissionIdentity(); 273 } 274 } 275 276 /** 277 * Polls until we're granted or denied a given permission. 278 */ pollForPermission(String perm, boolean granted)279 public static void pollForPermission(String perm, boolean granted) throws Exception { 280 pollForCondition(() -> granted == checkPermissionAndAppOp(perm), 281 "Timed out while waiting for permission " + perm + " to be " 282 + (granted ? "granted" : "revoked")); 283 } 284 285 286 /** 287 * Checks if the given {@code permission} is granted and corresponding AppOp is MODE_ALLOWED. 288 */ checkPermissionAndAppOp(String permission)289 private static boolean checkPermissionAndAppOp(String permission) { 290 final int pid = Os.getpid(); 291 final int uid = Os.getuid(); 292 final Context context = getContext(); 293 final String packageName = context.getPackageName(); 294 if (context.checkPermission(permission, pid, uid) != PackageManager.PERMISSION_GRANTED) { 295 return false; 296 } 297 298 final String op = AppOpsManager.permissionToOp(permission); 299 // No AppOp associated with the given permission, skip AppOp check. 300 if (op == null) { 301 return true; 302 } 303 304 final AppOpsManager appOps = context.getSystemService(AppOpsManager.class); 305 try { 306 appOps.checkPackage(uid, packageName); 307 } catch (SecurityException e) { 308 return false; 309 } 310 311 return appOps.unsafeCheckOpNoThrow(op, uid, packageName) == AppOpsManager.MODE_ALLOWED; 312 } 313 314 /** 315 * Installs a {@link TestApp} and grants it storage permissions. 316 */ installAppWithStoragePermissions(TestApp testApp)317 public static void installAppWithStoragePermissions(TestApp testApp) 318 throws Exception { 319 UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 320 try { 321 final String packageName = testApp.getPackageName(); 322 uiAutomation.adoptShellPermissionIdentity( 323 Manifest.permission.INSTALL_PACKAGES, Manifest.permission.DELETE_PACKAGES); 324 if (InstallUtils.getInstalledVersion(packageName) != -1) { 325 Uninstall.packages(packageName); 326 } 327 Install.single(testApp).commit(); 328 assertThat(InstallUtils.getInstalledVersion(packageName)).isEqualTo(1); 329 330 grantPermission(packageName, Manifest.permission.WRITE_EXTERNAL_STORAGE); 331 grantPermission(packageName, Manifest.permission.READ_EXTERNAL_STORAGE); 332 } finally { 333 uiAutomation.dropShellPermissionIdentity(); 334 } 335 } 336 337 /** 338 * Uninstalls a {@link TestApp}. 339 */ uninstallApp(TestApp testApp)340 public static void uninstallApp(TestApp testApp) throws Exception { 341 UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 342 try { 343 final String packageName = testApp.getPackageName(); 344 uiAutomation.adoptShellPermissionIdentity(Manifest.permission.DELETE_PACKAGES); 345 346 Uninstall.packages(packageName); 347 assertThat(InstallUtils.getInstalledVersion(packageName)).isEqualTo(-1); 348 } catch (Exception e) { 349 Log.e(TAG, "Exception occurred while uninstalling app: " + testApp, e); 350 } finally { 351 uiAutomation.dropShellPermissionIdentity(); 352 } 353 } 354 355 /** 356 * Makes the given {@code testApp} open a file for read or write. 357 * 358 * <p>This method drops shell permission identity. 359 */ openFileAs(TestApp testApp, File dirPath)360 public static ParcelFileDescriptor openFileAs(TestApp testApp, File dirPath) 361 throws Exception { 362 String actionName = getContext().getPackageName() + ".open_file"; 363 Bundle bundle = getFromTestApp(testApp, dirPath.getPath(), actionName); 364 return getContext().getContentResolver().openFileDescriptor( 365 bundle.getParcelable(actionName), "rw"); 366 } 367 368 /** 369 * <p>This method drops shell permission identity. 370 */ getFromTestApp(TestApp testApp, String dirPath, String actionName)371 private static Bundle getFromTestApp(TestApp testApp, String dirPath, String actionName) 372 throws Exception { 373 final CountDownLatch latch = new CountDownLatch(1); 374 final Bundle[] bundle = new Bundle[1]; 375 BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { 376 @Override 377 public void onReceive(Context context, Intent intent) { 378 bundle[0] = intent.getExtras(); 379 latch.countDown(); 380 } 381 }; 382 383 sendIntentToTestApp(testApp, dirPath, actionName, broadcastReceiver, latch); 384 return bundle[0]; 385 } 386 387 /** 388 * <p>This method drops shell permission identity. 389 */ sendIntentToTestApp(TestApp testApp, String dirPath, String actionName, BroadcastReceiver broadcastReceiver, CountDownLatch latch)390 private static void sendIntentToTestApp(TestApp testApp, String dirPath, String actionName, 391 BroadcastReceiver broadcastReceiver, CountDownLatch latch) throws Exception { 392 final String packageName = testApp.getPackageName(); 393 forceStopApp(packageName); 394 // Register broadcast receiver 395 final IntentFilter intentFilter = new IntentFilter(); 396 intentFilter.addAction(actionName); 397 intentFilter.addCategory(Intent.CATEGORY_DEFAULT); 398 getContext().registerReceiver(broadcastReceiver, intentFilter); 399 400 // Launch the test app. 401 final Intent intent = new Intent(Intent.ACTION_MAIN); 402 intent.setPackage(packageName); 403 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 404 intent.putExtra(INTENT_QUERY_TYPE, actionName); 405 intent.putExtra(INTENT_EXTRA_CALLING_PKG, getContext().getPackageName()); 406 intent.putExtra(INTENT_EXTRA_PATH, dirPath); 407 intent.addCategory(Intent.CATEGORY_LAUNCHER); 408 getContext().startActivity(intent); 409 if (!latch.await(POLLING_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { 410 final String errorMessage = "Timed out while waiting to receive " + actionName 411 + " intent from " + packageName; 412 throw new TimeoutException(errorMessage); 413 } 414 getContext().unregisterReceiver(broadcastReceiver); 415 } 416 417 /** 418 * <p>This method drops shell permission identity. 419 */ forceStopApp(String packageName)420 private static void forceStopApp(String packageName) throws Exception { 421 UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 422 try { 423 uiAutomation.adoptShellPermissionIdentity(Manifest.permission.FORCE_STOP_PACKAGES); 424 425 getContext().getSystemService(ActivityManager.class).forceStopPackage(packageName); 426 Thread.sleep(1000); 427 } finally { 428 uiAutomation.dropShellPermissionIdentity(); 429 } 430 } 431 assertFileContent(File file1, File file2, ParcelFileDescriptor pfd1, ParcelFileDescriptor pfd2, boolean assertSame)432 public static void assertFileContent(File file1, File file2, ParcelFileDescriptor pfd1, 433 ParcelFileDescriptor pfd2, boolean assertSame) throws Exception { 434 final int len = 1024; 435 byte[] bytes1; 436 byte[] bytes2; 437 int size1 = 0; 438 int size2 = 0; 439 440 boolean isSame = true; 441 do { 442 bytes1 = new byte[len]; 443 bytes2 = new byte[len]; 444 445 size1 = Os.read(pfd1.getFileDescriptor(), bytes1, 0, len); 446 size2 = Os.read(pfd2.getFileDescriptor(), bytes2, 0, len); 447 448 assertTrue(size1 >= 0); 449 assertTrue(size2 >= 0); 450 451 isSame = (size1 == size2) && Arrays.equals(bytes1, bytes2); 452 if (!isSame) { 453 break; 454 } 455 } while (size1 > 0 && size2 > 0); 456 457 String message = String.format("Files: %s and %s. isSame=%b. assertSame=%s", 458 file1, file2, isSame, assertSame); 459 assertEquals(message, isSame, assertSame); 460 } 461 assertTranscode(Uri uri, boolean transcode)462 public static void assertTranscode(Uri uri, boolean transcode) throws Exception { 463 long start = SystemClock.elapsedRealtimeNanos(); 464 assertTranscode(open(uri, true, null /* bundle */), transcode); 465 } 466 assertTranscode(File file, boolean transcode)467 public static void assertTranscode(File file, boolean transcode) throws Exception { 468 assertTranscode(open(file, false), transcode); 469 } 470 assertTranscode(ParcelFileDescriptor pfd, boolean transcode)471 public static void assertTranscode(ParcelFileDescriptor pfd, boolean transcode) 472 throws Exception { 473 long start = SystemClock.elapsedRealtimeNanos(); 474 assertEquals(10, Os.pread(pfd.getFileDescriptor(), new byte[10], 0, 10, 0)); 475 long end = SystemClock.elapsedRealtimeNanos(); 476 long readDuration = end - start; 477 478 // With transcoding read(2) > 100ms (usually > 1s) 479 // Without transcoding read(2) < 10ms (usually < 1ms) 480 String message = "readDuration=" + readDuration + "ns"; 481 if (transcode) { 482 assertTrue(message, readDuration > TimeUnit.MILLISECONDS.toNanos(100)); 483 } else { 484 assertTrue(message, readDuration < TimeUnit.MILLISECONDS.toNanos(10)); 485 } 486 } 487 isAppIoBlocked(StorageManager sm, UUID uuid)488 public static boolean isAppIoBlocked(StorageManager sm, UUID uuid) { 489 UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 490 uiAutomation.adoptShellPermissionIdentity("android.permission.WRITE_MEDIA_STORAGE"); 491 try { 492 return sm.isAppIoBlocked(uuid, Process.myUid(), Process.myTid(), 493 StorageManager.APP_IO_BLOCKED_REASON_TRANSCODING); 494 } finally { 495 uiAutomation.dropShellPermissionIdentity(); 496 } 497 } 498 isAVCHWEncoderSupported()499 public static boolean isAVCHWEncoderSupported() { 500 MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS); 501 for (MediaCodecInfo info : mcl.getCodecInfos()) { 502 if (info.isEncoder() && info.isVendor() && !info.getName().contains("secure") 503 && info.isHardwareAccelerated()) { 504 try { 505 CodecCapabilities caps = 506 info.getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_AVC); 507 } catch (IllegalArgumentException e) { 508 continue; 509 } 510 return true; 511 } 512 } 513 return false; 514 } 515 516 @NonNull getUiAutomation()517 private static UiAutomation getUiAutomation() { 518 return InstrumentationRegistry.getInstrumentation().getUiAutomation(); 519 } 520 } 521