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