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