| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.mediaprovidertranscode.cts; |
| |
| import static android.Manifest.permission.WRITE_DEVICE_CONFIG; |
| import static android.mediaprovidertranscode.cts.TranscodeTestConstants.INTENT_EXTRA_CALLING_PKG; |
| import static android.mediaprovidertranscode.cts.TranscodeTestConstants.INTENT_EXTRA_PATH; |
| import static android.mediaprovidertranscode.cts.TranscodeTestConstants.INTENT_QUERY_TYPE; |
| import static android.provider.DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT; |
| |
| import static androidx.test.InstrumentationRegistry.getContext; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertTrue; |
| |
| import android.Manifest; |
| import android.app.ActivityManager; |
| import android.app.AppOpsManager; |
| import android.app.UiAutomation; |
| import android.content.BroadcastReceiver; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.pm.PackageManager; |
| import android.media.MediaCodecInfo; |
| import android.media.MediaCodecInfo.CodecCapabilities; |
| import android.media.MediaCodecList; |
| import android.media.MediaFormat; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.Environment; |
| import android.os.FileUtils; |
| import android.os.ParcelFileDescriptor; |
| import android.os.Process; |
| import android.os.SystemClock; |
| import android.os.storage.StorageManager; |
| import android.provider.DeviceConfig; |
| import android.provider.MediaStore; |
| import android.system.Os; |
| import android.system.OsConstants; |
| import android.util.Log; |
| |
| import androidx.annotation.NonNull; |
| import androidx.test.InstrumentationRegistry; |
| |
| import com.android.cts.install.lib.Install; |
| import com.android.cts.install.lib.InstallUtils; |
| import com.android.cts.install.lib.TestApp; |
| import com.android.cts.install.lib.Uninstall; |
| |
| import com.google.common.io.ByteStreams; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InterruptedIOException; |
| import java.util.Arrays; |
| import java.util.UUID; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.TimeoutException; |
| import java.util.function.Supplier; |
| |
| public class TranscodeTestUtils { |
| private static final String TAG = "TranscodeTestUtils"; |
| |
| private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(20); |
| private static final long POLLING_SLEEP_MILLIS = 100; |
| private static final String TRANSCODE_COMPAT_MANIFEST_DEVICE_CONFIG_PROPERTY_NAME = |
| "transcode_compat_manifest"; |
| |
| public static Uri stageHEVCVideoFile(File videoFile) throws IOException { |
| return stageVideoFile(videoFile, R.raw.testvideo_HEVC); |
| } |
| |
| public static Uri stageSmallHevcVideoFile(File videoFile) throws IOException { |
| return stageVideoFile(videoFile, R.raw.testVideo_HEVC_small); |
| } |
| |
| public static Uri stageMediumHevcVideoFile(File videoFile) throws IOException { |
| return stageVideoFile(videoFile, R.raw.testVideo_HEVC_medium); |
| } |
| |
| public static Uri stageLongHevcVideoFile(File videoFile) throws IOException { |
| return stageVideoFile(videoFile, R.raw.testVideo_HEVC_long); |
| } |
| |
| public static Uri stageLegacyVideoFile(File videoFile) throws IOException { |
| return stageVideoFile(videoFile, R.raw.testVideo_Legacy); |
| } |
| |
| private static Uri stageVideoFile(File videoFile, int resourceId) throws IOException { |
| if (!videoFile.getParentFile().exists()) { |
| assertTrue(videoFile.getParentFile().mkdirs()); |
| } |
| try (InputStream in = |
| getContext().getResources().openRawResource(resourceId); |
| FileOutputStream out = new FileOutputStream(videoFile)) { |
| FileUtils.copy(in, out); |
| // Sync file to disk to ensure file is fully written to the lower fs before scanning |
| // Otherwise, media provider might try to read the file on the lower fs and not see |
| // the fully written bytes |
| out.getFD().sync(); |
| } |
| return MediaStore.scanFile(getContext().getContentResolver(), videoFile); |
| } |
| |
| public static ParcelFileDescriptor open(File file, boolean forWrite) throws Exception { |
| return ParcelFileDescriptor.open(file, forWrite ? ParcelFileDescriptor.MODE_READ_WRITE |
| : ParcelFileDescriptor.MODE_READ_ONLY); |
| } |
| |
| public static ParcelFileDescriptor open(Uri uri, boolean forWrite, Bundle bundle) |
| throws Exception { |
| ContentResolver resolver = getContext().getContentResolver(); |
| if (bundle == null) { |
| return resolver.openFileDescriptor(uri, forWrite ? "rw" : "r"); |
| } else { |
| return resolver.openTypedAssetFileDescriptor(uri, "*/*", bundle) |
| .getParcelFileDescriptor(); |
| } |
| } |
| |
| static byte[] read(ParcelFileDescriptor parcelFileDescriptor, int byteCount, int fileOffset) |
| throws Exception { |
| assertThat(byteCount).isGreaterThan(-1); |
| assertThat(fileOffset).isGreaterThan(-1); |
| |
| Os.lseek(parcelFileDescriptor.getFileDescriptor(), fileOffset, OsConstants.SEEK_SET); |
| |
| byte[] bytes = new byte[byteCount]; |
| int numBytesRead = Os.read(parcelFileDescriptor.getFileDescriptor(), bytes, |
| 0 /* byteOffset */, byteCount); |
| assertThat(numBytesRead).isGreaterThan(-1); |
| return bytes; |
| } |
| |
| static void write(ParcelFileDescriptor parcelFileDescriptor, byte[] bytes, int byteCount, |
| int fileOffset) throws Exception { |
| assertThat(byteCount).isGreaterThan(-1); |
| assertThat(fileOffset).isGreaterThan(-1); |
| |
| Os.lseek(parcelFileDescriptor.getFileDescriptor(), fileOffset, OsConstants.SEEK_SET); |
| |
| int numBytesWritten = Os.write(parcelFileDescriptor.getFileDescriptor(), bytes, |
| 0 /* byteOffset */, byteCount); |
| assertThat(numBytesWritten).isNotEqualTo(-1); |
| assertThat(numBytesWritten).isEqualTo(byteCount); |
| } |
| |
| public static void enableTranscodingForPackage(String packageName) { |
| getUiAutomation().adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG); |
| try { |
| final String newPropertyValue = packageName + ",0"; |
| DeviceConfig.setProperty(NAMESPACE_STORAGE_NATIVE_BOOT, |
| TRANSCODE_COMPAT_MANIFEST_DEVICE_CONFIG_PROPERTY_NAME, newPropertyValue, |
| /* makeDefault */ false); |
| } finally { |
| getUiAutomation().dropShellPermissionIdentity(); |
| } |
| SystemClock.sleep(1000); |
| } |
| |
| public static void forceEnableAppCompatHevc(String packageName) throws IOException { |
| final String command = "am compat enable 174228127 " + packageName; |
| executeShellCommand(command); |
| } |
| |
| public static void forceDisableAppCompatHevc(String packageName) throws IOException { |
| final String command = "am compat enable 174227820 " + packageName; |
| executeShellCommand(command); |
| } |
| |
| public static void resetAppCompat(String packageName) throws IOException { |
| final String command = "am compat reset-all " + packageName; |
| executeShellCommand(command); |
| } |
| |
| public static void disableTranscodingForAllPackages() { |
| getUiAutomation().adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG); |
| try { |
| DeviceConfig.deleteProperty(NAMESPACE_STORAGE_NATIVE_BOOT, |
| TRANSCODE_COMPAT_MANIFEST_DEVICE_CONFIG_PROPERTY_NAME); |
| } finally { |
| getUiAutomation().dropShellPermissionIdentity(); |
| } |
| SystemClock.sleep(1000); |
| } |
| |
| /** |
| * Executes a shell command. |
| */ |
| public static String executeShellCommand(String command) throws IOException { |
| int attempt = 0; |
| while (attempt++ < 5) { |
| try { |
| return executeShellCommandInternal(command); |
| } catch (InterruptedIOException e) { |
| // Hmm, we had trouble executing the shell command; the best we |
| // can do is try again a few more times |
| Log.v(TAG, "Trouble executing " + command + "; trying again", e); |
| } |
| } |
| throw new IOException("Failed to execute " + command); |
| } |
| |
| private static String executeShellCommandInternal(String cmd) throws IOException { |
| UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); |
| try (FileInputStream output = new FileInputStream( |
| uiAutomation.executeShellCommand(cmd).getFileDescriptor())) { |
| return new String(ByteStreams.toByteArray(output)); |
| } |
| } |
| |
| /** |
| * Polls for external storage to be mounted. |
| */ |
| public static void pollForExternalStorageState() throws Exception { |
| pollForCondition( |
| () -> Environment.getExternalStorageState(Environment.getExternalStorageDirectory()) |
| .equals(Environment.MEDIA_MOUNTED), |
| "Timed out while waiting for ExternalStorageState to be MEDIA_MOUNTED"); |
| } |
| |
| private static void pollForCondition(Supplier<Boolean> condition, String errorMessage) |
| throws Exception { |
| for (int i = 0; i < POLLING_TIMEOUT_MILLIS / POLLING_SLEEP_MILLIS; i++) { |
| if (condition.get()) { |
| return; |
| } |
| Thread.sleep(POLLING_SLEEP_MILLIS); |
| } |
| throw new TimeoutException(errorMessage); |
| } |
| |
| public static void grantPermission(String packageName, String permission) { |
| UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); |
| uiAutomation.adoptShellPermissionIdentity("android.permission.GRANT_RUNTIME_PERMISSIONS"); |
| try { |
| uiAutomation.grantRuntimePermission(packageName, permission); |
| } finally { |
| uiAutomation.dropShellPermissionIdentity(); |
| } |
| } |
| |
| /** |
| * Polls until we're granted or denied a given permission. |
| */ |
| public static void pollForPermission(String perm, boolean granted) throws Exception { |
| pollForCondition(() -> granted == checkPermissionAndAppOp(perm), |
| "Timed out while waiting for permission " + perm + " to be " |
| + (granted ? "granted" : "revoked")); |
| } |
| |
| |
| /** |
| * Checks if the given {@code permission} is granted and corresponding AppOp is MODE_ALLOWED. |
| */ |
| private static boolean checkPermissionAndAppOp(String permission) { |
| final int pid = Os.getpid(); |
| final int uid = Os.getuid(); |
| final Context context = getContext(); |
| final String packageName = context.getPackageName(); |
| if (context.checkPermission(permission, pid, uid) != PackageManager.PERMISSION_GRANTED) { |
| return false; |
| } |
| |
| final String op = AppOpsManager.permissionToOp(permission); |
| // No AppOp associated with the given permission, skip AppOp check. |
| if (op == null) { |
| return true; |
| } |
| |
| final AppOpsManager appOps = context.getSystemService(AppOpsManager.class); |
| try { |
| appOps.checkPackage(uid, packageName); |
| } catch (SecurityException e) { |
| return false; |
| } |
| |
| return appOps.unsafeCheckOpNoThrow(op, uid, packageName) == AppOpsManager.MODE_ALLOWED; |
| } |
| |
| /** |
| * Installs a {@link TestApp} and grants it storage permissions. |
| */ |
| public static void installAppWithStoragePermissions(TestApp testApp) |
| throws Exception { |
| UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); |
| try { |
| final String packageName = testApp.getPackageName(); |
| uiAutomation.adoptShellPermissionIdentity( |
| Manifest.permission.INSTALL_PACKAGES, Manifest.permission.DELETE_PACKAGES); |
| if (InstallUtils.getInstalledVersion(packageName) != -1) { |
| Uninstall.packages(packageName); |
| } |
| Install.single(testApp).commit(); |
| assertThat(InstallUtils.getInstalledVersion(packageName)).isEqualTo(1); |
| |
| grantPermission(packageName, Manifest.permission.WRITE_EXTERNAL_STORAGE); |
| grantPermission(packageName, Manifest.permission.READ_EXTERNAL_STORAGE); |
| } finally { |
| uiAutomation.dropShellPermissionIdentity(); |
| } |
| } |
| |
| /** |
| * Uninstalls a {@link TestApp}. |
| */ |
| public static void uninstallApp(TestApp testApp) throws Exception { |
| UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); |
| try { |
| final String packageName = testApp.getPackageName(); |
| uiAutomation.adoptShellPermissionIdentity(Manifest.permission.DELETE_PACKAGES); |
| |
| Uninstall.packages(packageName); |
| assertThat(InstallUtils.getInstalledVersion(packageName)).isEqualTo(-1); |
| } catch (Exception e) { |
| Log.e(TAG, "Exception occurred while uninstalling app: " + testApp, e); |
| } finally { |
| uiAutomation.dropShellPermissionIdentity(); |
| } |
| } |
| |
| /** |
| * Makes the given {@code testApp} open a file for read or write. |
| * |
| * <p>This method drops shell permission identity. |
| */ |
| public static ParcelFileDescriptor openFileAs(TestApp testApp, File dirPath) |
| throws Exception { |
| String actionName = getContext().getPackageName() + ".open_file"; |
| Bundle bundle = getFromTestApp(testApp, dirPath.getPath(), actionName); |
| return getContext().getContentResolver().openFileDescriptor( |
| bundle.getParcelable(actionName), "rw"); |
| } |
| |
| /** |
| * <p>This method drops shell permission identity. |
| */ |
| private static Bundle getFromTestApp(TestApp testApp, String dirPath, String actionName) |
| throws Exception { |
| final CountDownLatch latch = new CountDownLatch(1); |
| final Bundle[] bundle = new Bundle[1]; |
| BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| bundle[0] = intent.getExtras(); |
| latch.countDown(); |
| } |
| }; |
| |
| sendIntentToTestApp(testApp, dirPath, actionName, broadcastReceiver, latch); |
| return bundle[0]; |
| } |
| |
| /** |
| * <p>This method drops shell permission identity. |
| */ |
| private static void sendIntentToTestApp(TestApp testApp, String dirPath, String actionName, |
| BroadcastReceiver broadcastReceiver, CountDownLatch latch) throws Exception { |
| final String packageName = testApp.getPackageName(); |
| forceStopApp(packageName); |
| // Register broadcast receiver |
| final IntentFilter intentFilter = new IntentFilter(); |
| intentFilter.addAction(actionName); |
| intentFilter.addCategory(Intent.CATEGORY_DEFAULT); |
| getContext().registerReceiver(broadcastReceiver, intentFilter); |
| |
| // Launch the test app. |
| final Intent intent = new Intent(Intent.ACTION_MAIN); |
| intent.setPackage(packageName); |
| intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| intent.putExtra(INTENT_QUERY_TYPE, actionName); |
| intent.putExtra(INTENT_EXTRA_CALLING_PKG, getContext().getPackageName()); |
| intent.putExtra(INTENT_EXTRA_PATH, dirPath); |
| intent.addCategory(Intent.CATEGORY_LAUNCHER); |
| getContext().startActivity(intent); |
| if (!latch.await(POLLING_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { |
| final String errorMessage = "Timed out while waiting to receive " + actionName |
| + " intent from " + packageName; |
| throw new TimeoutException(errorMessage); |
| } |
| getContext().unregisterReceiver(broadcastReceiver); |
| } |
| |
| /** |
| * <p>This method drops shell permission identity. |
| */ |
| private static void forceStopApp(String packageName) throws Exception { |
| UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); |
| try { |
| uiAutomation.adoptShellPermissionIdentity(Manifest.permission.FORCE_STOP_PACKAGES); |
| |
| getContext().getSystemService(ActivityManager.class).forceStopPackage(packageName); |
| Thread.sleep(1000); |
| } finally { |
| uiAutomation.dropShellPermissionIdentity(); |
| } |
| } |
| |
| public static void assertFileContent(File file1, File file2, ParcelFileDescriptor pfd1, |
| ParcelFileDescriptor pfd2, boolean assertSame) throws Exception { |
| final int len = 1024; |
| byte[] bytes1; |
| byte[] bytes2; |
| int size1 = 0; |
| int size2 = 0; |
| |
| boolean isSame = true; |
| do { |
| bytes1 = new byte[len]; |
| bytes2 = new byte[len]; |
| |
| size1 = Os.read(pfd1.getFileDescriptor(), bytes1, 0, len); |
| size2 = Os.read(pfd2.getFileDescriptor(), bytes2, 0, len); |
| |
| assertTrue(size1 >= 0); |
| assertTrue(size2 >= 0); |
| |
| isSame = (size1 == size2) && Arrays.equals(bytes1, bytes2); |
| if (!isSame) { |
| break; |
| } |
| } while (size1 > 0 && size2 > 0); |
| |
| String message = String.format("Files: %s and %s. isSame=%b. assertSame=%s", |
| file1, file2, isSame, assertSame); |
| assertEquals(message, isSame, assertSame); |
| } |
| |
| public static void assertTranscode(Uri uri, boolean transcode) throws Exception { |
| long start = SystemClock.elapsedRealtimeNanos(); |
| assertTranscode(open(uri, true, null /* bundle */), transcode); |
| } |
| |
| public static void assertTranscode(File file, boolean transcode) throws Exception { |
| assertTranscode(open(file, false), transcode); |
| } |
| |
| public static void assertTranscode(ParcelFileDescriptor pfd, boolean transcode) |
| throws Exception { |
| long start = SystemClock.elapsedRealtimeNanos(); |
| assertEquals(10, Os.pread(pfd.getFileDescriptor(), new byte[10], 0, 10, 0)); |
| long end = SystemClock.elapsedRealtimeNanos(); |
| long readDuration = end - start; |
| |
| // With transcoding read(2) > 100ms (usually > 1s) |
| // Without transcoding read(2) < 10ms (usually < 1ms) |
| String message = "readDuration=" + readDuration + "ns"; |
| if (transcode) { |
| assertTrue(message, readDuration > TimeUnit.MILLISECONDS.toNanos(100)); |
| } else { |
| assertTrue(message, readDuration < TimeUnit.MILLISECONDS.toNanos(10)); |
| } |
| } |
| |
| public static boolean isAppIoBlocked(StorageManager sm, UUID uuid) { |
| UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); |
| uiAutomation.adoptShellPermissionIdentity("android.permission.WRITE_MEDIA_STORAGE"); |
| try { |
| return sm.isAppIoBlocked(uuid, Process.myUid(), Process.myTid(), |
| StorageManager.APP_IO_BLOCKED_REASON_TRANSCODING); |
| } finally { |
| uiAutomation.dropShellPermissionIdentity(); |
| } |
| } |
| |
| public static boolean isAVCHWEncoderSupported() { |
| MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS); |
| for (MediaCodecInfo info : mcl.getCodecInfos()) { |
| if (info.isEncoder() && info.isVendor() && !info.getName().contains("secure") |
| && info.isHardwareAccelerated()) { |
| try { |
| CodecCapabilities caps = |
| info.getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_AVC); |
| } catch (IllegalArgumentException e) { |
| continue; |
| } |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @NonNull |
| private static UiAutomation getUiAutomation() { |
| return InstrumentationRegistry.getInstrumentation().getUiAutomation(); |
| } |
| } |