Screen capture (mediaProjection) on Android 14

2.4k views Asked by At

I'm trying to adapt my app to run on Android 14. It worked fine on Android 13. But in SDK version 34 I get an exception when I try to start the foreground service.

Caused by: java.lang.SecurityException: 
Starting FGS with type mediaProjection callerApp=ProcessRecord
targetSDK=34 requires permissions: all of the permissions allOf=true
[android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION] 
any of the permissions allOf=false 
[android.permission.CAPTURE_VIDEO_OUTPUT, android:project_media]
at android.os.Parcel.createExceptionOrNull(Parcel.java:3057)
at android.os.Parcel.createException(Parcel.java:3041)
at android.os.Parcel.readException(Parcel.java:3024)
at android.os.Parcel.readException(Parcel.java:2966)
at android.app.IActivityManager$Stub$Proxy.setServiceForeground(IActivityManager.java:6761)
at android.app.Service.startForeground(Service.java:862)
at .service.ScreenCaptureService.startWithNotification(ScreenCaptureService.java:147)

This message looks like I need to get permission to access the camera. But I don't use the camera at all in my app.

Doc:

The app must set the foregroundServiceType attribute to FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION in the element of the app's manifest file. For an app targeting SDK version U or later, the user must have granted the app with the permission to start a projection, before the app starts a foreground service with the type android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION. Additionally, the app must have started the foreground service with that type before calling this API here, or else it'll receive a SecurityException from this API call, unless it's a privileged app.

Manifest:

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />

<service
   android:name=".service.ScreenCaptureService"
   android:enabled="true"
   android:exported="false"
   android:foregroundServiceType="mediaProjection"
   android:stopWithTask="false" />

My actions:

  1. Create ScreenCaptureService.
  2. Requesting user permission to capture screen.
  3. If permission is granted, the foreground service starts.
public int onStartCommand(Intent intent, int flags, int startId) {
   final String intent_action = intent.getAction();

   switch (intent_action) {
      case ACTION_START_SERVICE:
        _is_alive = true;
        _window_name = intent.getStringExtra(EXTRA_WINDOW_NAME);
        startActivity(new Intent(this, ScreenCapturePermissionActivity.class)
        .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
           break;
      case ACTION_PERMISSION_RESULT:
        handleScreenCapPermResult();
           break;
      case ACTION_STOP_SERVICE:
      default:
        freeServiceResources();
   }

   return START_STICKY;
}
private void startWithNotification() {
        Notification notification =
                new NotificationCompat.Builder(
                    App.context(), 
                    getString(R.string.sys_notification_chan_ID))
                .setSmallIcon(R.drawable.ic_foreground_notification)
                .setOngoing(true)
                .setContentText(getString(R.string.foreground_service_message))
                .setCategory(Notification.CATEGORY_SERVICE)
                .build();


        if (Build.VERSION.SDK_INT > 28)
            startForeground(
              1, 
              notification, 
              ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
        else
            startForeground(1, notification);
}

UPDATE: I think this is a bug in the SDK that comes with Android Studio. ContextCompat.checkSelfPermission always returns PERMISSION_DENIED regardless of when permission is requested. I checked all launch options. The result is always the same. And mediaProjection does not start with the received intent.

protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);

   ActivityResultLauncher<Intent> startMediaProjection =
      registerForActivityResult(new ActivityResultContracts
             .StartActivityForResult(),
             result -> {
                if (result.getResultCode() == RESULT_OK) {
                    Log.e("DEBUG", "RESULT_OK");
                    if (android.os.Build.VERSION.SDK_INT > 33) {
                       int perm_code = 
                           ContextCompat.checkSelfPermission(
                           this,
                           Manifest
                            .permission
                             .FOREGROUND_SERVICE_MEDIA_PROJECTION);
                       if (perm_code == PackageManager.PERMISSION_GRANTED) {
                         Log.e("DEBUG", "PERMISSION_GRANTED");
                       } else {
                         Log.e("DEBUG", "PERMISSION_DENIED");                 
                       }
                   } else {
                     Log.e("DEBUG", "PERMISSION_GRANTED");
                   }
              else {
                 Log.e("DEBUG", "RESULT_CANCELED");
              }

     creenCaptureService.handlePermission();
     finish();
     });

  final MediaProjectionManager manager =
                     getSystemService(MediaProjectionManager.class);    
  startMediaProjection.launch(manager.createScreenCaptureIntent());
}
3

There are 3 answers

0
Kolodieiev On BEST ANSWER

Instead of

<uses-permission android:name="Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />

Now we need to ask for permission

<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />

Nine days of searching... I love Android developers. Without any warning in the manual, the permission was moved to a new package, while the old one was not marked as deprecated.

2
徐燕军 On
0
gaobiaoqing On

I have the same issue you must start your service in onActivityResult Callback function you can see:https://developer.android.com/guide/topics/large-screens/media-projection#legacy_approach;

If the user provides confirmation, startActivityForResult() passes a result code and data to the onActivityResult() callback.

You can then pass the data and result code to the getMediaProjection() method from MediaProjectionManager to create an instance of MediaProjection

init service also must in onActivityResult

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data)

    if (requestCode == 2) {
        if (hasInitService.not()) {
            initService()
            hasInitService = true
        }