• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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