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.TestApp; 64 import com.android.modules.utils.build.SdkLevel; 65 66 import com.google.common.io.ByteStreams; 67 68 import java.io.File; 69 import java.io.FileInputStream; 70 import java.io.FileOutputStream; 71 import java.io.IOException; 72 import java.io.InputStream; 73 import java.io.InterruptedIOException; 74 import java.util.Arrays; 75 import java.util.UUID; 76 import java.util.concurrent.CountDownLatch; 77 import java.util.concurrent.TimeUnit; 78 import java.util.concurrent.TimeoutException; 79 import java.util.function.Supplier; 80 81 public class TranscodeTestUtils { 82 private static final String TAG = "TranscodeTestUtils"; 83 84 private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(20); 85 private static final long POLLING_SLEEP_MILLIS = 100; 86 private static final String TRANSCODE_COMPAT_MANIFEST_DEVICE_CONFIG_PROPERTY_NAME = 87 "transcode_compat_manifest"; 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 if (SdkLevel.isAtLeastU()) { 170 getUiAutomation().adoptShellPermissionIdentity(WRITE_ALLOWLISTED_DEVICE_CONFIG); 171 try { 172 final String newPropertyValue = packageName + ",0"; 173 DeviceConfig.setProperty(NAMESPACE_STORAGE_NATIVE_BOOT, 174 TRANSCODE_COMPAT_MANIFEST_DEVICE_CONFIG_PROPERTY_NAME, newPropertyValue, 175 /* makeDefault */ false); 176 } finally { 177 getUiAutomation().dropShellPermissionIdentity(); 178 } 179 } else { 180 executeShellCommand("device_config put storage_native_boot transcode_compat_manifest " 181 + packageName + ",0"); 182 } 183 SystemClock.sleep(1000); 184 } 185 forceEnableAppCompatHevc(String packageName)186 public static void forceEnableAppCompatHevc(String packageName) throws IOException { 187 final String command = "am compat enable 174228127 " + packageName; 188 executeShellCommand(command); 189 } 190 forceDisableAppCompatHevc(String packageName)191 public static void forceDisableAppCompatHevc(String packageName) throws IOException { 192 final String command = "am compat enable 174227820 " + packageName; 193 executeShellCommand(command); 194 } 195 resetAppCompat(String packageName)196 public static void resetAppCompat(String packageName) throws IOException { 197 final String command = "am compat reset-all " + packageName; 198 executeShellCommand(command); 199 } 200 disableTranscodingForAllPackages()201 public static void disableTranscodingForAllPackages() throws Exception { 202 if (SdkLevel.isAtLeastU()) { 203 getUiAutomation().adoptShellPermissionIdentity(WRITE_ALLOWLISTED_DEVICE_CONFIG); 204 try { 205 DeviceConfig.deleteProperty(NAMESPACE_STORAGE_NATIVE_BOOT, 206 TRANSCODE_COMPAT_MANIFEST_DEVICE_CONFIG_PROPERTY_NAME); 207 } finally { 208 getUiAutomation().dropShellPermissionIdentity(); 209 } 210 } else { 211 executeShellCommand("device_config delete storage_native_boot " 212 + "transcode_compat_manifest"); 213 } 214 SystemClock.sleep(1000); 215 } 216 217 /** 218 * Executes a shell command. 219 */ executeShellCommand(String command)220 public static String executeShellCommand(String command) throws IOException { 221 int attempt = 0; 222 while (attempt++ < 5) { 223 try { 224 return executeShellCommandInternal(command); 225 } catch (InterruptedIOException e) { 226 // Hmm, we had trouble executing the shell command; the best we 227 // can do is try again a few more times 228 Log.v(TAG, "Trouble executing " + command + "; trying again", e); 229 } 230 } 231 throw new IOException("Failed to execute " + command); 232 } 233 executeShellCommandInternal(String cmd)234 private static String executeShellCommandInternal(String cmd) throws IOException { 235 UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 236 try (FileInputStream output = new FileInputStream( 237 uiAutomation.executeShellCommand(cmd).getFileDescriptor())) { 238 return new String(ByteStreams.toByteArray(output)); 239 } 240 } 241 242 /** 243 * Polls for external storage to be mounted. 244 */ pollForExternalStorageState()245 public static void pollForExternalStorageState() throws Exception { 246 pollForCondition( 247 () -> Environment.getExternalStorageState(Environment.getExternalStorageDirectory()) 248 .equals(Environment.MEDIA_MOUNTED), 249 "Timed out while waiting for ExternalStorageState to be MEDIA_MOUNTED"); 250 } 251 pollForCondition(Supplier<Boolean> condition, String errorMessage)252 private static void pollForCondition(Supplier<Boolean> condition, String errorMessage) 253 throws Exception { 254 for (int i = 0; i < POLLING_TIMEOUT_MILLIS / POLLING_SLEEP_MILLIS; i++) { 255 if (condition.get()) { 256 return; 257 } 258 Thread.sleep(POLLING_SLEEP_MILLIS); 259 } 260 throw new TimeoutException(errorMessage); 261 } 262 grantPermission(String packageName, String permission)263 public static void grantPermission(String packageName, String permission) { 264 UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 265 uiAutomation.adoptShellPermissionIdentity("android.permission.GRANT_RUNTIME_PERMISSIONS"); 266 try { 267 uiAutomation.grantRuntimePermission(packageName, permission); 268 } finally { 269 uiAutomation.dropShellPermissionIdentity(); 270 } 271 } 272 273 /** 274 * Polls until we're granted or denied a given permission. 275 */ pollForPermission(String perm, boolean granted)276 public static void pollForPermission(String perm, boolean granted) throws Exception { 277 pollForCondition(() -> granted == checkPermissionAndAppOp(perm), 278 "Timed out while waiting for permission " + perm + " to be " 279 + (granted ? "granted" : "revoked")); 280 } 281 282 283 /** 284 * Checks if the given {@code permission} is granted and corresponding AppOp is MODE_ALLOWED. 285 */ checkPermissionAndAppOp(String permission)286 private static boolean checkPermissionAndAppOp(String permission) { 287 final int pid = Os.getpid(); 288 final int uid = Os.getuid(); 289 final Context context = getContext(); 290 final String packageName = context.getPackageName(); 291 if (context.checkPermission(permission, pid, uid) != PackageManager.PERMISSION_GRANTED) { 292 return false; 293 } 294 295 final String op = AppOpsManager.permissionToOp(permission); 296 // No AppOp associated with the given permission, skip AppOp check. 297 if (op == null) { 298 return true; 299 } 300 301 final AppOpsManager appOps = context.getSystemService(AppOpsManager.class); 302 try { 303 appOps.checkPackage(uid, packageName); 304 } catch (SecurityException e) { 305 return false; 306 } 307 308 return appOps.unsafeCheckOpNoThrow(op, uid, packageName) == AppOpsManager.MODE_ALLOWED; 309 } 310 311 /** 312 * Makes the given {@code testApp} open a file for read or write. 313 * 314 * <p>This method drops shell permission identity. 315 */ openFileAs(TestApp testApp, File dirPath)316 public static ParcelFileDescriptor openFileAs(TestApp testApp, File dirPath) 317 throws Exception { 318 String actionName = getContext().getPackageName() + ".open_file"; 319 Bundle bundle = getFromTestApp(testApp, dirPath.getPath(), actionName); 320 return getContext().getContentResolver().openFileDescriptor( 321 bundle.getParcelable(actionName), "rw"); 322 } 323 324 /** 325 * <p>This method drops shell permission identity. 326 */ getFromTestApp(TestApp testApp, String dirPath, String actionName)327 private static Bundle getFromTestApp(TestApp testApp, String dirPath, String actionName) 328 throws Exception { 329 final CountDownLatch latch = new CountDownLatch(1); 330 final Bundle[] bundle = new Bundle[1]; 331 BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { 332 @Override 333 public void onReceive(Context context, Intent intent) { 334 bundle[0] = intent.getExtras(); 335 latch.countDown(); 336 } 337 }; 338 339 sendIntentToTestApp(testApp, dirPath, actionName, broadcastReceiver, latch); 340 return bundle[0]; 341 } 342 343 /** 344 * <p>This method drops shell permission identity. 345 */ sendIntentToTestApp(TestApp testApp, String dirPath, String actionName, BroadcastReceiver broadcastReceiver, CountDownLatch latch)346 private static void sendIntentToTestApp(TestApp testApp, String dirPath, String actionName, 347 BroadcastReceiver broadcastReceiver, CountDownLatch latch) throws Exception { 348 final String packageName = testApp.getPackageName(); 349 forceStopApp(packageName); 350 // Register broadcast receiver 351 final IntentFilter intentFilter = new IntentFilter(); 352 intentFilter.addAction(actionName); 353 intentFilter.addCategory(Intent.CATEGORY_DEFAULT); 354 getContext().registerReceiver(broadcastReceiver, intentFilter); 355 356 // Launch the test app. 357 final Intent intent = new Intent(Intent.ACTION_MAIN); 358 intent.setPackage(packageName); 359 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 360 intent.putExtra(INTENT_QUERY_TYPE, actionName); 361 intent.putExtra(INTENT_EXTRA_CALLING_PKG, getContext().getPackageName()); 362 intent.putExtra(INTENT_EXTRA_PATH, dirPath); 363 intent.addCategory(Intent.CATEGORY_LAUNCHER); 364 getContext().startActivity(intent); 365 if (!latch.await(POLLING_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { 366 final String errorMessage = "Timed out while waiting to receive " + actionName 367 + " intent from " + packageName; 368 throw new TimeoutException(errorMessage); 369 } 370 getContext().unregisterReceiver(broadcastReceiver); 371 } 372 373 /** 374 * <p>This method drops shell permission identity. 375 */ forceStopApp(String packageName)376 private static void forceStopApp(String packageName) throws Exception { 377 UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 378 try { 379 uiAutomation.adoptShellPermissionIdentity(Manifest.permission.FORCE_STOP_PACKAGES); 380 381 getContext().getSystemService(ActivityManager.class).forceStopPackage(packageName); 382 Thread.sleep(1000); 383 } finally { 384 uiAutomation.dropShellPermissionIdentity(); 385 } 386 } 387 assertFileContent(File file1, File file2, ParcelFileDescriptor pfd1, ParcelFileDescriptor pfd2, boolean assertSame)388 public static void assertFileContent(File file1, File file2, ParcelFileDescriptor pfd1, 389 ParcelFileDescriptor pfd2, boolean assertSame) throws Exception { 390 final int len = 1024; 391 byte[] bytes1; 392 byte[] bytes2; 393 int size1 = 0; 394 int size2 = 0; 395 396 boolean isSame = true; 397 do { 398 bytes1 = new byte[len]; 399 bytes2 = new byte[len]; 400 401 size1 = Os.read(pfd1.getFileDescriptor(), bytes1, 0, len); 402 size2 = Os.read(pfd2.getFileDescriptor(), bytes2, 0, len); 403 404 assertTrue(size1 >= 0); 405 assertTrue(size2 >= 0); 406 407 isSame = (size1 == size2) && Arrays.equals(bytes1, bytes2); 408 if (!isSame) { 409 break; 410 } 411 } while (size1 > 0 && size2 > 0); 412 413 String message = String.format("Files: %s and %s. isSame=%b. assertSame=%s", 414 file1, file2, isSame, assertSame); 415 assertEquals(message, isSame, assertSame); 416 } 417 assertTranscode(Uri uri, boolean transcode)418 public static void assertTranscode(Uri uri, boolean transcode) throws Exception { 419 long start = SystemClock.elapsedRealtimeNanos(); 420 assertTranscode(open(uri, true, null /* bundle */), transcode); 421 } 422 assertTranscode(File file, boolean transcode)423 public static void assertTranscode(File file, boolean transcode) throws Exception { 424 assertTranscode(open(file, false), transcode); 425 } 426 assertTranscode(ParcelFileDescriptor pfd, boolean transcode)427 public static void assertTranscode(ParcelFileDescriptor pfd, boolean transcode) 428 throws Exception { 429 long start = SystemClock.elapsedRealtimeNanos(); 430 assertEquals(10, Os.pread(pfd.getFileDescriptor(), new byte[10], 0, 10, 0)); 431 long end = SystemClock.elapsedRealtimeNanos(); 432 long readDuration = end - start; 433 434 // With transcoding read(2) > 100ms (usually > 1s) 435 // Without transcoding read(2) < 10ms (usually < 1ms) 436 String message = "readDuration=" + readDuration + "ns"; 437 if (transcode) { 438 assertTrue(message, readDuration > TimeUnit.MILLISECONDS.toNanos(100)); 439 } else { 440 assertTrue(message, readDuration < TimeUnit.MILLISECONDS.toNanos(10)); 441 } 442 } 443 isAppIoBlocked(StorageManager sm, UUID uuid)444 public static boolean isAppIoBlocked(StorageManager sm, UUID uuid) { 445 UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 446 uiAutomation.adoptShellPermissionIdentity("android.permission.WRITE_MEDIA_STORAGE"); 447 try { 448 return sm.isAppIoBlocked(uuid, Process.myUid(), Process.myTid(), 449 StorageManager.APP_IO_BLOCKED_REASON_TRANSCODING); 450 } finally { 451 uiAutomation.dropShellPermissionIdentity(); 452 } 453 } 454 isAVCHWEncoderSupported()455 public static boolean isAVCHWEncoderSupported() { 456 MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS); 457 for (MediaCodecInfo info : mcl.getCodecInfos()) { 458 if (info.isEncoder() && info.isVendor() && !info.getName().contains("secure") 459 && info.isHardwareAccelerated()) { 460 try { 461 CodecCapabilities caps = 462 info.getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_AVC); 463 } catch (IllegalArgumentException e) { 464 continue; 465 } 466 return true; 467 } 468 } 469 return false; 470 } 471 472 @NonNull getUiAutomation()473 private static UiAutomation getUiAutomation() { 474 return InstrumentationRegistry.getInstrumentation().getUiAutomation(); 475 } 476 } 477