• 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 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