26

Problem: I have a screenshot app that uses a floating overlay service for controls, and screen cast API Media Project Manager to get access to the screen. Sometimes when a device is low on memory Android restarts the service, and I lose my media projection.

The only way I know of to reacquire a new media projection is to re-open an Activity that requests the permissions, and that would be the end of it, except for one problem. Certain apps, particularly games, seem to listen for when they lose the foreground process, and pause, or otherwise reset. This is annoying.

Here's my ideal scenerio. Service opens, if the user has selected " checked don't ask me again" in permission request dialog, it gets the media projection in a way that does not disturb the current foreground activity.

How do I get a media projection manager without disturbing the current foreground process?

Is there either a way to get media projection from a straight service, or a way to open activity in the background from a service?

Currently I use this code in Activity to get the MediaProjectionManager

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
protected void getScreenShotPermission() {
    if (isLollipopOrNewer) {
        mediaProjectionManager = (MediaProjectionManager) getContext().getSystemService(MEDIA_PROJECTION_SERVICE);
        startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(), 1);
    }
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == 1) {
        if (resultCode == Activity.RESULT_OK) {
            mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data);
            this.finish();
        }
    }
}
netsplit
  • 1,070
  • 1
  • 10
  • 18
  • "it gets the media projection in a way that does not disturb the current foreground activity" -- well, the decision of whether they are "disturbed" is up to the developers of the app in the foreground. If there is a way to take screenshots without popping up the authorization activity, that's a bug that needs to be fixed. And since when your service dies, your overlay dies with it, it would seem that you have other re-initialization issues. I'd just a `Notification`, and use `createScreenCaptureIntent()` and restart your overlay when the user taps on the `Notification`. – CommonsWare Oct 28 '15 at 17:59
  • I'm currently using a notification for some of the controls, including turning the service on or off. – netsplit Oct 28 '15 at 18:01
  • Currently in Lollipop and Marshmallow users have the option of "don't ask me again" which sets it to either automatically grant or deny without a dialogue for that. This is by design I believe, so not a bug (I hope!). I haven't seen the actual overlay disturb an app yet, just the permission request process – netsplit Oct 28 '15 at 18:05
  • 1
    Well, the overlay won't disturb the foreground app, because "disturb" will usually be driven by activity lifecycles, and your overlay is not an activity. With regards to the checkbox, not only do you need to handle the case where the user hasn't checked that, but AFAIK that `startActivityForResult()` call is unavoidable (and hence can trigger lifecycle events in the foreground activity). – CommonsWare Oct 28 '15 at 18:10
  • It being unavoidable as part of the life cycle is what I'm afraid of, but if that's the answer then that's the answer. The case where the user hasn't checked the checkbox is okay. If a user wants to be asked every time, than dialogs are part of that. Ty! – netsplit Oct 28 '15 at 18:22

2 Answers2

34

So I came back to this because it was dumb and it was bugging me, and I figured it out!

In another class (in mine it's the application class) put this code:

private static Intent screenshotPermission = null;

protected static void getScreenshotPermission() {
    try {
        if (hasScreenshotPermission()) {
            if(null != mediaProjection) {
                mediaProjection.stop();
                mediaProjection = null;
            }
            mediaProjection = mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, (Intent) screenshotPermission.clone()); 
        } else {
            openScreenshotPermissionRequester();
        }
    } catch (final RuntimeException ignored) {
        openScreenshotPermissionRequester();
    }
}

protected static void openScreenshotPermissionRequester(){
    final Intent intent = new Intent(context, AcquireScreenshotPermissionIntent.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    context.startActivity(intent);
}



protected static void setScreenshotPermission(final Intent permissionIntent) {
    screenshotPermission = permissionIntent;
}

In your activity class handling the initial request (in my case: AcquireScreenshotPermissionIntent) put this code in your onactivityresult:

@Override
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (1 == requestCode) {
        if (Activity.RESULT_OK == resultCode) {
            setScreenshotPermission((Intent) data.clone());
        }
    } else if (Activity.RESULT_CANCELED == resultCode) {
        setScreenshotPermission(null);
        log("no access");

    }
    finish();

Simply call getScreenShotPermission() whenever you need permission, then use the resulting mediaProjection object.

Here's how it works: The magic token is some data included in the Intent. What I tried initially was putting the result intent a global variable and using it to create the media projection from a nonactivity class. Problem is it would fail. What I eventually figured out is the token gets consumed when you create a media projection with it. Passing it as an argument or assigning to a new variable just passes a pointer to it, and it still gets consumed.

What you need to do instead is use object.clone();. This makes a new copy of the token, the new copy gets consumed, and you can create additional tokens as needed, as long as you don't consume the original. As a bonus your app only has to ask for screenshot permission once per launch. If something else takes over the screencast, or the Android memory manager gets you, you're covered. You can create a new virtual screen without sending onPause or onStop events to other apps.

netsplit
  • 1,070
  • 1
  • 10
  • 18
  • 2
    How do you pass your media projection to a service? – James Andrew Apr 02 '16 at 16:52
  • You pass the resulting intent object (called data in my example) to the service. The service would then use the clone() method to make copies of the intent as needed to create screencasts. You must never use the original object, or it'll be consumed, and you'll need to re-request screencast permission to get another. – netsplit Apr 04 '16 at 21:09
  • 1
    The way I passed it to the service was by storing in a class variable (using the static keyword in it's deceleration) with a protected class method for the service class to access it. – netsplit Apr 04 '16 at 21:11
  • 1
    There's probably other ways to do it though, this is what worked for me – netsplit Apr 04 '16 at 21:17
  • 1
    The method in my example works once per app start, so restarting your phone would naturally restart the app and trigger the permission request sequence. – netsplit Apr 10 '16 at 15:49
  • Is it possible to check if the app has the permission to use the MediaProjectionManager, without opening its activity using ProjectionManager.createScreenCaptureIntent() ? – android developer Apr 27 '17 at 09:41
  • if using the above code, and the screenshotPermission Intent isn't null then you should have it. – netsplit May 10 '17 at 13:55
  • 3
    can you share the complete code , I am new to this and would like to learn more, what is hasScreenshotPermission(), Activity.RESULT_OK, can your share the class files with the relevant code , easy to learn – 1234567 Jul 06 '17 at 11:40
  • 1
    Since "screenshotPermission" doesn't get changed, why would you clone it? – android developer Jul 06 '17 at 23:00
  • can any one share complete code of this , just the getting permission part , this code is a bit incomplete , I need it urgently for learning it , have been looking for it for about a week – 1234567 Jul 07 '17 at 03:09
  • 1
    This will not work as you can use one permission only one time. The next time you want to start MediaProjection you have to ask for a new permission intent. – LPVOID Mar 25 '19 at 14:04
  • this will work once but after restarting device how can i get the intent data?? – Pooja Shukla Apr 16 '19 at 07:58
  • 1
    super bro you made my day Thank you very much – Machhindra Neupane Aug 07 '20 at 16:36
  • Pooja Shukla, restarting the device will destroy the token. There might be way to preserve it using serialization or some other storage technique, but I suspect Android security explicitly blocks that. All is not lost however. If there user selects "never ask me again" when prompted, it'll remember their decision. If they choose to grant your app permission, then future requests will be granted without a prompt – netsplit Aug 31 '20 at 18:26
  • Can you share your repository of your source code I am stuck in a problem – RanaUmer Feb 12 '21 at 18:14
  • thanks for showing the cloning to avoid showing the promt again – binarynoise May 20 '21 at 15:27
  • Unfortunately this doesn't work anymore with android 14-beta :(. Is there any workaround? – Tosin John Jul 25 '23 at 11:26
3

I have a continues running service so i declared a one static variable inside the service and use it when user click my floating button (like chatheads).

public static Intent data = null;  //declaration 

then when first time user open the app I am asking for the permission and setting the value of the data in following way.

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {

    if (requestCode == 4) {
        Log.d(TAG, "onActivityResult: for Screen Capture");
        if (resultCode == RESULT_OK) {
            Log.d(TAG, "onActivityResult: Permission Granted");
            WindowChangeDetectingService.data = (Intent) data.clone();
            screenCapturePermission = true;
            updateUI();
        } else {
            Log.d(TAG, "onActivityResult: Permission Deined");
            screenCapturePermission = false;
            updateUI();
            SnackBar screenCapturePermissionSnackbar = new SnackBar(this, "This Permission is needed.", "Grant", new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    takeScreenCapturePermission();
                }
            });
            screenCapturePermissionSnackbar.setDismissTimer(2000);
            screenCapturePermissionSnackbar.show();
        }
    }
}

Finally when user clicks on the floating Overlay Button, first check value of the data.

if (WindowChangeDetectingService.data == null) {
    Log.d(TAG, "basicSetup: Asking permission again");
    //open app again
} else {
    Log.d(TAG, "basicSetup: Getting permission for the object");
    readPermissionObject();
}

Implementation of readPermissionObject is as follows

private void readPermissionObject() {
    Intent data =  WindowChangeDetectingService.getData();
    mMediaProjectionManager  = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
    startProjection(Activity.RESULT_OK,data);
}

Thanks to @netsplit for giving info about data.clone(); .

Sarthak Doshi
  • 424
  • 4
  • 8