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