1 /*
2  * Copyright (C) 2022 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.media.codec.cts;
18 
19 import static android.media.codec.cts.MediaCodecResourceTestHighPriorityActivity.ACTION_HIGH_PRIORITY_ACTIVITY_READY;
20 import static android.media.codec.cts.MediaCodecResourceTestLowPriorityService.ACTION_LOW_PRIORITY_SERVICE_READY;
21 
22 import static org.junit.Assert.assertFalse;
23 import static org.junit.Assert.assertTrue;
24 import static org.junit.Assert.fail;
25 import static org.junit.Assume.assumeTrue;
26 
27 import android.Manifest;
28 import android.content.BroadcastReceiver;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.IntentFilter;
32 import android.media.MediaCodec;
33 import android.media.MediaCodec.CodecException;
34 import android.media.MediaCodecInfo;
35 import android.media.MediaCodecInfo.CodecCapabilities;
36 import android.media.MediaCodecInfo.VideoCapabilities;
37 import android.media.MediaCodecList;
38 import android.media.MediaFormat;
39 import android.os.Build;
40 import android.platform.test.annotations.RequiresDevice;
41 import android.util.Log;
42 
43 import androidx.annotation.Nullable;
44 import androidx.test.filters.SdkSuppress;
45 import androidx.test.filters.SmallTest;
46 import androidx.test.platform.app.InstrumentationRegistry;
47 
48 import com.android.compatibility.common.util.ApiTest;
49 
50 import org.junit.Test;
51 
52 import java.util.ArrayList;
53 import java.util.List;
54 
55 // This class verifies the resource management aspects of MediaCodecs.
56 @SmallTest
57 @RequiresDevice
58 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
59 public class MediaCodecResourceTest {
60     private static final String TAG = "MediaCodecResourceTest";
61 
62     // Codec information that is pertinent to creating codecs for resource management testing.
63     private static class CodecInfo {
CodecInfo(String name, int maxSupportedInstances, String mime, MediaFormat mediaFormat)64         CodecInfo(String name, int maxSupportedInstances, String mime, MediaFormat mediaFormat) {
65             this.name = name;
66             this.maxSupportedInstances = maxSupportedInstances;
67             this.mime = mime;
68             this.mediaFormat = mediaFormat;
69         }
70         public final String name;
71         public final int maxSupportedInstances;
72         public final String mime;
73         public final MediaFormat mediaFormat;
74     }
75 
76     private static class ProcessInfo {
ProcessInfo(int pid, int uid)77         ProcessInfo(int pid, int uid) {
78             this.pid = pid;
79             this.uid = uid;
80         }
81         public final int pid;
82         public final int uid;
83     }
84 
85     @ApiTest(apis = "MediaCodec#createByCodecNameForClient")
86     @Test
testCreateCodecForAnotherProcessWithoutPermissionsThrows()87     public void testCreateCodecForAnotherProcessWithoutPermissionsThrows() throws Exception {
88         CodecInfo codecInfo = getFirstVideoHardwareDecoder();
89         assumeTrue("No video hardware codec found.", codecInfo != null);
90         try {
91             ProcessInfo processInfo = createLowPriorityProcess();
92             assertTrue("Unable to retrieve low priority process info.", processInfo != null);
93 
94             MediaCodec mediaCodec = MediaCodec.createByCodecNameForClient(codecInfo.name,
95                     processInfo.pid, processInfo.uid);
96             fail("No SecurityException thrown when creating a codec for another process");
97         } catch (SecurityException ex) {
98             // expected
99         } finally {
100             destroyLowPriorityProcess();
101             // Allow time for resources to be released
102             Thread.sleep(500);
103         }
104     }
105 
106     // A process with lower priority (e.g. background app) should not be able to reclaim
107     // MediaCodec resources from a process with higher priority (e.g. foreground app).
108     @ApiTest(apis = "MediaCodec#createByCodecNameForClient")
109     @Test
testLowerPriorityProcessFailsToReclaimResources()110     public void testLowerPriorityProcessFailsToReclaimResources() throws Exception {
111         CodecInfo codecInfo = getFirstVideoHardwareDecoder();
112         assumeTrue("No video hardware codec found.", codecInfo != null);
113         assertTrue("Expected at least one max supported codec instance.",
114                 codecInfo.maxSupportedInstances > 0);
115 
116         List<MediaCodec> mediaCodecList = new ArrayList<>();
117         try {
118             ProcessInfo lowPriorityProcess = createLowPriorityProcess();
119             assertTrue("Unable to retrieve low priority process info.", lowPriorityProcess != null);
120             ProcessInfo highPriorityProcess = createHighPriorityProcess();
121             assertTrue("Unable to retrieve high priority process info.",
122                     highPriorityProcess != null);
123 
124             // This permission is required to create MediaCodecs on behalf of other processes.
125             InstrumentationRegistry.getInstrumentation().getUiAutomation()
126                     .adoptShellPermissionIdentity(Manifest.permission.MEDIA_RESOURCE_OVERRIDE_PID);
127 
128             Log.i(TAG, "Creating MediaCodecs on behalf of pid " + highPriorityProcess.pid);
129             // Create more codecs than are supported by the device on behalf of a high-priority
130             // process.
131             boolean wasInitialInsufficientResourcesExceptionThrown = false;
132             for (int i = 0; i <= codecInfo.maxSupportedInstances; ++i) {
133                 try {
134                     MediaCodec mediaCodec = MediaCodec.createByCodecNameForClient(codecInfo.name,
135                             highPriorityProcess.pid, highPriorityProcess.uid);
136                     mediaCodecList.add(mediaCodec);
137                     mediaCodec.configure(codecInfo.mediaFormat, /* surface= */ null,
138                             /* crypto= */ null, /* flags= */ 0);
139                     mediaCodec.start();
140                 } catch (MediaCodec.CodecException ex) {
141                     if (ex.getErrorCode() == CodecException.ERROR_INSUFFICIENT_RESOURCE) {
142                         Log.i(TAG, "Exception received on MediaCodec #" +  i + ".");
143                         wasInitialInsufficientResourcesExceptionThrown = true;
144                     } else {
145                         Log.e(TAG, "Unexpected exception thrown", ex);
146                         throw ex;
147                     }
148                 }
149             }
150             // For the same process, insufficient resources should be thrown.
151             assertTrue(String.format("No MediaCodec.Exception thrown with insufficient"
152                     + " resources after creating too many %d codecs for %s on behalf of the"
153                     + " same process", codecInfo.maxSupportedInstances, codecInfo.name),
154                     wasInitialInsufficientResourcesExceptionThrown);
155 
156             Log.i(TAG, "Creating MediaCodecs on behalf of pid " + lowPriorityProcess.pid);
157             // Attempt to create the codec again, but this time, on behalf of a low priority
158             // process.
159             boolean wasLowPriorityInsufficientResourcesExceptionThrown = false;
160             try {
161                 MediaCodec mediaCodec = MediaCodec.createByCodecNameForClient(codecInfo.name,
162                         lowPriorityProcess.pid, lowPriorityProcess.uid);
163                 mediaCodecList.add(mediaCodec);
164                 mediaCodec.configure(codecInfo.mediaFormat, /* surface= */ null, /* crypto= */ null,
165                         /* flags= */ 0);
166                 mediaCodec.start();
167             } catch (MediaCodec.CodecException ex) {
168                 if (ex.getErrorCode() == CodecException.ERROR_INSUFFICIENT_RESOURCE) {
169                     wasLowPriorityInsufficientResourcesExceptionThrown = true;
170                 } else {
171                     Log.e(TAG, "Unexpected exception thrown", ex);
172                     throw ex;
173                 }
174             }
175             assertTrue(String.format("No MediaCodec.Exception thrown with insufficient"
176                     + " resources after creating a follow-up codec for %s on behalf of a lower"
177                     + " priority process", codecInfo.mime),
178                     wasLowPriorityInsufficientResourcesExceptionThrown);
179         } finally {
180             Log.i(TAG, "Cleaning up MediaCodecs");
181             for (MediaCodec mediaCodec : mediaCodecList) {
182                 mediaCodec.release();
183             }
184             destroyHighPriorityProcess();
185             destroyLowPriorityProcess();
186             // Allow time for the codecs and other resources to be released
187             Thread.sleep(500);
188             InstrumentationRegistry.getInstrumentation().getUiAutomation()
189                     .dropShellPermissionIdentity();
190         }
191     }
192 
193     // A process with higher priority (e.g. foreground app) should be able to reclaim
194     // MediaCodec resources from a process with lower priority (e.g. background app).
195     @ApiTest(apis = "MediaCodec#createByCodecNameForClient")
196     @Test
testHigherPriorityProcessReclaimsResources()197     public void testHigherPriorityProcessReclaimsResources() throws Exception {
198         CodecInfo codecInfo = getFirstVideoHardwareDecoder();
199         assumeTrue("No video hardware codec found.", codecInfo != null);
200         assertTrue("Expected at least one max supported codec instance.",
201                 codecInfo.maxSupportedInstances > 0);
202 
203         List<MediaCodec> mediaCodecList = new ArrayList<>();
204         try {
205             ProcessInfo lowPriorityProcess = createLowPriorityProcess();
206             assertTrue("Unable to retrieve low priority process info.", lowPriorityProcess != null);
207             ProcessInfo highPriorityProcess = createHighPriorityProcess();
208             assertTrue("Unable to retrieve high priority process info.",
209                     highPriorityProcess != null);
210 
211             // This permission is required to create MediaCodecs on behalf of other processes.
212             InstrumentationRegistry.getInstrumentation().getUiAutomation()
213                     .adoptShellPermissionIdentity(Manifest.permission.MEDIA_RESOURCE_OVERRIDE_PID);
214 
215             Log.i(TAG, "Creating MediaCodecs on behalf of pid " + lowPriorityProcess.pid);
216             // Create more codecs than are supported by the device on behalf of a low-priority
217             // process.
218             boolean wasInitialInsufficientResourcesExceptionThrown = false;
219             for (int i = 0; i <= codecInfo.maxSupportedInstances; ++i) {
220                 try {
221                     MediaCodec mediaCodec = MediaCodec.createByCodecNameForClient(codecInfo.name,
222                             lowPriorityProcess.pid, lowPriorityProcess.uid);
223                     mediaCodecList.add(mediaCodec);
224                     mediaCodec.configure(codecInfo.mediaFormat, /* surface= */ null,
225                             /* crypto= */ null, /* flags= */ 0);
226                     mediaCodec.start();
227                 } catch (MediaCodec.CodecException ex) {
228                     if (ex.getErrorCode() == CodecException.ERROR_INSUFFICIENT_RESOURCE) {
229                         Log.i(TAG, "Exception received on MediaCodec #" +  i + ".");
230                         wasInitialInsufficientResourcesExceptionThrown = true;
231                     } else {
232                         Log.e(TAG, "Unexpected exception thrown", ex);
233                         throw ex;
234                     }
235                 }
236             }
237             // For the same process, insufficient resources should be thrown.
238             assertTrue(String.format("No MediaCodec.Exception thrown with insufficient"
239                     + " resources after creating too many %d codecs for %s on behalf of the"
240                     + " same process", codecInfo.maxSupportedInstances, codecInfo.mime),
241                     wasInitialInsufficientResourcesExceptionThrown);
242 
243             Log.i(TAG, "Creating final MediaCodec on behalf of pid " + highPriorityProcess.pid);
244             // Attempt to create the codec again, but this time, on behalf of a high-priority
245             // process.
246             boolean wasHighPriorityInsufficientResourcesExceptionThrown = false;
247             try {
248                 MediaCodec mediaCodec = MediaCodec.createByCodecNameForClient(codecInfo.name,
249                         highPriorityProcess.pid, highPriorityProcess.uid);
250                 mediaCodecList.add(mediaCodec);
251                 mediaCodec.configure(codecInfo.mediaFormat, /* surface= */ null, /* crypto= */ null,
252                         /* flags= */ 0);
253                 mediaCodec.start();
254             } catch (MediaCodec.CodecException ex) {
255                 if (ex.getErrorCode() == CodecException.ERROR_INSUFFICIENT_RESOURCE) {
256                     wasHighPriorityInsufficientResourcesExceptionThrown = true;
257                 } else {
258                     Log.e(TAG, "Unexpected exception thrown", ex);
259                     throw ex;
260                 }
261             }
262             assertFalse(String.format("Resource reclaiming should occur when creating a"
263                     + " follow-up codec for %s on behalf of a higher priority process, but"
264                     + " received an insufficient resource CodecException instead",
265                     codecInfo.mime), wasHighPriorityInsufficientResourcesExceptionThrown);
266         } finally {
267             Log.i(TAG, "Cleaning up MediaCodecs");
268             for (MediaCodec mediaCodec : mediaCodecList) {
269                 mediaCodec.release();
270             }
271             destroyHighPriorityProcess();
272             destroyLowPriorityProcess();
273             // Allow time for the codecs and other resources to be released
274             Thread.sleep(500);
275             InstrumentationRegistry.getInstrumentation().getUiAutomation()
276                 .dropShellPermissionIdentity();
277         }
278     }
279 
280     // Find the first hardware video decoder and create a media format for it.
281     @Nullable
getFirstVideoHardwareDecoder()282     private CodecInfo getFirstVideoHardwareDecoder() {
283         MediaCodecList allMediaCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
284         for (MediaCodecInfo mediaCodecInfo : allMediaCodecList.getCodecInfos()) {
285             if (mediaCodecInfo.isSoftwareOnly()) {
286                 continue;
287             }
288             if (mediaCodecInfo.isEncoder()) {
289                 continue;
290             }
291             if (mediaCodecInfo.getSupportedTypes().length == 0) {
292                 continue;
293             }
294             String mime = mediaCodecInfo.getSupportedTypes()[0];
295             CodecCapabilities codecCapabilities = mediaCodecInfo.getCapabilitiesForType(mime);
296             VideoCapabilities videoCapabilities = codecCapabilities.getVideoCapabilities();
297             if (videoCapabilities != null) {
298                 int height = videoCapabilities.getSupportedHeights().getLower();
299                 int width = videoCapabilities.getSupportedWidthsFor(height).getLower();
300                 MediaFormat mediaFormat = new MediaFormat();
301                 mediaFormat.setString(MediaFormat.KEY_MIME, mime);
302                 mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, height);
303                 mediaFormat.setInteger(MediaFormat.KEY_WIDTH, width);
304                 return new CodecInfo(mediaCodecInfo.getName(),
305                         codecCapabilities.getMaxSupportedInstances(), mime, mediaFormat);
306             }
307         }
308         return null;
309     }
310 
createLowPriorityProcess()311     private ProcessInfo createLowPriorityProcess() {
312         Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
313         ProcessInfoBroadcastReceiver processInfoBroadcastReceiver =
314                 new ProcessInfoBroadcastReceiver();
315         context.registerReceiver(processInfoBroadcastReceiver,
316                 new IntentFilter(ACTION_LOW_PRIORITY_SERVICE_READY),
317                 Context.RECEIVER_EXPORTED_UNAUDITED);
318         Intent intent = new Intent(context, MediaCodecResourceTestLowPriorityService.class);
319         context.startForegroundService(intent);
320         // Starting the service and receiving the broadcast should take less than 5 seconds
321         ProcessInfo processInfo = processInfoBroadcastReceiver.waitForProcessInfoMs(5000);
322         context.unregisterReceiver(processInfoBroadcastReceiver);
323         return processInfo;
324     }
325 
createHighPriorityProcess()326     private ProcessInfo createHighPriorityProcess() {
327         Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
328         ProcessInfoBroadcastReceiver processInfoBroadcastReceiver =
329                 new ProcessInfoBroadcastReceiver();
330         context.registerReceiver(processInfoBroadcastReceiver,
331                 new IntentFilter(ACTION_HIGH_PRIORITY_ACTIVITY_READY),
332                 Context.RECEIVER_EXPORTED_UNAUDITED);
333         Intent intent = new Intent(context, MediaCodecResourceTestHighPriorityActivity.class);
334         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
335         context.startActivity(intent);
336         // Starting the activity and receiving the broadcast should take less than 5 seconds
337         ProcessInfo processInfo = processInfoBroadcastReceiver.waitForProcessInfoMs(5000);
338         context.unregisterReceiver(processInfoBroadcastReceiver);
339         return processInfo;
340     }
341 
destroyLowPriorityProcess()342     private void destroyLowPriorityProcess() {
343         Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
344         Intent intent = new Intent(context, MediaCodecResourceTestLowPriorityService.class);
345         context.stopService(intent);
346     }
347 
destroyHighPriorityProcess()348     private void destroyHighPriorityProcess() {
349         Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
350         Intent intent = new Intent().setAction(
351                 MediaCodecResourceTestHighPriorityActivity.ACTION_HIGH_PRIORITY_ACTIVITY_FINISH);
352         context.sendBroadcast(intent);
353     }
354 
355     private static class ProcessInfoBroadcastReceiver extends BroadcastReceiver {
356         private int mPid = -1;
357         private int mUid = -1;
358 
359         @Override
onReceive(Context context, Intent intent)360         public void onReceive(Context context, Intent intent) {
361             synchronized (this) {
362                 mPid = intent.getIntExtra("pid", -1);
363                 mUid = intent.getIntExtra("uid", -1);
364                 this.notify();
365             }
366         }
367 
waitForProcessInfoMs(int milliseconds)368         public ProcessInfo waitForProcessInfoMs(int milliseconds) {
369             synchronized (this) {
370                 try {
371                     this.wait(milliseconds);
372                 } catch (InterruptedException ex) {
373                     return null;
374                 }
375             }
376             if (mPid == -1 || mUid == -1) {
377                 return null;
378             }
379             return new ProcessInfo(mPid, mUid);
380         }
381     }
382 }
383