Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6d8bf2ab authored by Naomi Musgrave's avatar Naomi Musgrave Committed by Automerger Merge Worker
Browse files

[MediaProjection] Re-show permission dialog when token reuse am: 0a66fbb6

parents 4e46d94f 0a66fbb6
Loading
Loading
Loading
Loading
+31 −0
Original line number Diff line number Diff line
@@ -20,7 +20,10 @@ import android.content.Intent
import android.content.res.Configuration
import android.content.res.Resources
import android.media.projection.IMediaProjection
import android.media.projection.IMediaProjectionManager.EXTRA_USER_REVIEW_GRANTED_CONSENT
import android.media.projection.MediaProjectionManager.EXTRA_MEDIA_PROJECTION
import android.media.projection.ReviewGrantedConsentResult.RECORD_CANCEL
import android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_TASK
import android.os.Binder
import android.os.Bundle
import android.os.IBinder
@@ -67,6 +70,11 @@ class MediaProjectionAppSelectorActivity(
    private lateinit var controller: MediaProjectionAppSelectorController
    private lateinit var recentsViewController: MediaProjectionRecentsViewController
    private lateinit var component: MediaProjectionAppSelectorComponent
    // Indicate if we are under the media projection security flow
    // i.e. when a host app reuses consent token, review the permission and update it to the service
    private var reviewGrantedConsentRequired = false
    // If an app is selected, set to true so that we don't send RECORD_CANCEL in onDestroy
    private var taskSelected = false

    override fun getLayoutResource() = R.layout.media_projection_app_selector

@@ -85,6 +93,9 @@ class MediaProjectionAppSelectorActivity(
            component.personalProfileUserHandle
        )

        reviewGrantedConsentRequired =
            intent.getBooleanExtra(EXTRA_USER_REVIEW_GRANTED_CONSENT, false)

        super.onCreate(bundle)
        controller.init()
    }
@@ -149,6 +160,16 @@ class MediaProjectionAppSelectorActivity(
    }

    override fun onDestroy() {
        // onDestroy is also called when an app is selected, in that case we only want to send
        // RECORD_CONTENT_TASK but not RECORD_CANCEL
        if (!taskSelected) {
            // TODO(b/272010156): Return result to PermissionActivity and update service there
            MediaProjectionServiceHelper.setReviewedConsentIfNeeded(
                RECORD_CANCEL,
                reviewGrantedConsentRequired,
                /* projection= */ null
            )
        }
        activityLauncher.destroy()
        controller.destroy()
        super.onDestroy()
@@ -163,6 +184,7 @@ class MediaProjectionAppSelectorActivity(
    }

    override fun returnSelectedApp(launchCookie: IBinder) {
        taskSelected = true
        if (intent.hasExtra(EXTRA_CAPTURE_REGION_RESULT_RECEIVER)) {
            // The client requested to return the result in the result receiver instead of
            // activity result, let's send the media projection to the result receiver
@@ -174,7 +196,11 @@ class MediaProjectionAppSelectorActivity(
            val captureRegion = MediaProjectionCaptureTarget(launchCookie)
            val data = Bundle().apply { putParcelable(KEY_CAPTURE_TARGET, captureRegion) }
            resultReceiver.send(RESULT_OK, data)
            // TODO(b/279175710): Ensure consent result is always set here. Skipping this for now
            //  in ScreenMediaRecorder, since we know the permission grant (projection) is never
            //  reused in that scenario.
        } else {
            // TODO(b/272010156): Return result to PermissionActivity and update service there
            // Return the media projection instance as activity result
            val mediaProjectionBinder = intent.getIBinderExtra(EXTRA_MEDIA_PROJECTION)
            val projection = IMediaProjection.Stub.asInterface(mediaProjectionBinder)
@@ -185,6 +211,11 @@ class MediaProjectionAppSelectorActivity(
            intent.putExtra(EXTRA_MEDIA_PROJECTION, projection.asBinder())
            setResult(RESULT_OK, intent)
            setForceSendResultForMediaProjection()
            MediaProjectionServiceHelper.setReviewedConsentIfNeeded(
                RECORD_CONTENT_TASK,
                reviewGrantedConsentRequired,
                projection
            )
        }

        finish()
+74 −32
Original line number Diff line number Diff line
@@ -16,11 +16,16 @@

package com.android.systemui.media;

import static android.media.projection.IMediaProjectionManager.EXTRA_PACKAGE_REUSING_GRANTED_CONSENT;
import static android.media.projection.IMediaProjectionManager.EXTRA_USER_REVIEW_GRANTED_CONSENT;
import static android.media.projection.ReviewGrantedConsentResult.RECORD_CANCEL;
import static android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_DISPLAY;
import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;

import static com.android.systemui.screenrecord.ScreenShareOptionKt.ENTIRE_SCREEN;
import static com.android.systemui.screenrecord.ScreenShareOptionKt.SINGLE_APP;

import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.AlertDialog;
@@ -30,12 +35,10 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.Typeface;
import android.media.projection.IMediaProjection;
import android.media.projection.IMediaProjectionManager;
import android.media.projection.MediaProjectionManager;
import android.media.projection.ReviewGrantedConsentResult;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.text.BidiFormatter;
import android.text.SpannableString;
@@ -55,10 +58,10 @@ import com.android.systemui.screenrecord.ScreenShareOption;
import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.util.Utils;

import javax.inject.Inject;

import dagger.Lazy;

import javax.inject.Inject;

public class MediaProjectionPermissionActivity extends Activity
        implements DialogInterface.OnClickListener {
    private static final String TAG = "MediaProjectionPermissionActivity";
@@ -70,10 +73,13 @@ public class MediaProjectionPermissionActivity extends Activity

    private String mPackageName;
    private int mUid;
    private IMediaProjectionManager mService;

    private AlertDialog mDialog;

    // Indicates if user must review already-granted consent that the MediaProjection app is
    // attempting to re-use.
    private boolean mReviewGrantedConsentRequired = false;

    @Inject
    public MediaProjectionPermissionActivity(FeatureFlags featureFlags,
            Lazy<ScreenCaptureDevicePolicyResolver> screenCaptureDevicePolicyResolver) {
@@ -85,14 +91,24 @@ public class MediaProjectionPermissionActivity extends Activity
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);

        final Intent launchingIntent = getIntent();
        mReviewGrantedConsentRequired = launchingIntent.getBooleanExtra(
                EXTRA_USER_REVIEW_GRANTED_CONSENT, false);

        mPackageName = getCallingPackage();
        IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE);
        mService = IMediaProjectionManager.Stub.asInterface(b);

        // This activity is launched directly by an app, or system server. System server provides
        // the package name through the intent if so.
        if (mPackageName == null) {
            finish();
            if (launchingIntent.hasExtra(EXTRA_PACKAGE_REUSING_GRANTED_CONSENT)) {
                mPackageName = launchingIntent.getStringExtra(
                        EXTRA_PACKAGE_REUSING_GRANTED_CONSENT);
            } else {
                setResult(RESULT_CANCELED);
                finish(RECORD_CANCEL, /* projection= */ null);
                return;
            }
        }

        PackageManager packageManager = getPackageManager();
        ApplicationInfo aInfo;
@@ -100,25 +116,36 @@ public class MediaProjectionPermissionActivity extends Activity
            aInfo = packageManager.getApplicationInfo(mPackageName, 0);
            mUid = aInfo.uid;
        } catch (PackageManager.NameNotFoundException e) {
            Log.e(TAG, "unable to look up package name", e);
            finish();
            Log.e(TAG, "Unable to look up package name", e);
            setResult(RESULT_CANCELED);
            finish(RECORD_CANCEL, /* projection= */ null);
            return;
        }

        try {
            if (mService.hasProjectionPermission(mUid, mPackageName)) {
                setResult(RESULT_OK, getMediaProjectionIntent(mUid, mPackageName));
                finish();
            if (MediaProjectionServiceHelper.hasProjectionPermission(mUid, mPackageName)) {
                final IMediaProjection projection =
                        MediaProjectionServiceHelper.createOrReuseProjection(mUid, mPackageName,
                                mReviewGrantedConsentRequired);
                // Automatically grant consent if a system-privileged component is recording.
                final Intent intent = new Intent();
                intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION,
                        projection.asBinder());
                setResult(RESULT_OK, intent);
                finish(RECORD_CONTENT_DISPLAY, projection);
                return;
            }
        } catch (RemoteException e) {
            Log.e(TAG, "Error checking projection permissions", e);
            finish();
            setResult(RESULT_CANCELED);
            finish(RECORD_CANCEL, /* projection= */ null);
            return;
        }

        if (mFeatureFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES)) {
            if (showScreenCaptureDisabledDialogIfNeeded()) {
                setResult(RESULT_CANCELED);
                finish(RECORD_CANCEL, /* projection= */ null);
                return;
            }
        }
@@ -178,7 +205,7 @@ public class MediaProjectionPermissionActivity extends Activity
                ScreenShareOption selectedOption =
                        ((MediaProjectionPermissionDialog) mDialog).getSelectedScreenShareOption();
                grantMediaProjectionPermission(selectedOption.getMode());
            }, appName);
            }, () -> finish(RECORD_CANCEL, /* projection= */ null), appName);
        } else {
            AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this,
                    R.style.Theme_SystemUI_Dialog)
@@ -191,7 +218,6 @@ public class MediaProjectionPermissionActivity extends Activity
        }

        setUpDialog(mDialog);

        mDialog.show();
    }

@@ -207,6 +233,12 @@ public class MediaProjectionPermissionActivity extends Activity
    public void onClick(DialogInterface dialog, int which) {
        if (which == AlertDialog.BUTTON_POSITIVE) {
            grantMediaProjectionPermission(ENTIRE_SCREEN);
        } else {
            if (mDialog != null) {
                mDialog.dismiss();
            }
            setResult(RESULT_CANCELED);
            finish(RECORD_CANCEL, /* projection= */ null);
        }
    }

@@ -240,15 +272,25 @@ public class MediaProjectionPermissionActivity extends Activity
    private void grantMediaProjectionPermission(int screenShareMode) {
        try {
            if (screenShareMode == ENTIRE_SCREEN) {
                setResult(RESULT_OK, getMediaProjectionIntent(mUid, mPackageName));
                final IMediaProjection projection =
                        MediaProjectionServiceHelper.createOrReuseProjection(mUid, mPackageName,
                                mReviewGrantedConsentRequired);
                final Intent intent = new Intent();
                intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION,
                        projection.asBinder());
                setResult(RESULT_OK, intent);
                finish(RECORD_CONTENT_DISPLAY, projection);
            }
            if (isPartialScreenSharingEnabled() && screenShareMode == SINGLE_APP) {
                IMediaProjection projection = createProjection(mUid, mPackageName);
                final Intent intent = new Intent(this, MediaProjectionAppSelectorActivity.class);
                IMediaProjection projection = MediaProjectionServiceHelper.createOrReuseProjection(
                        mUid, mPackageName, mReviewGrantedConsentRequired);
                final Intent intent = new Intent(this,
                        MediaProjectionAppSelectorActivity.class);
                intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION,
                        projection.asBinder());
                intent.putExtra(MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE,
                        getHostUserHandle());
                intent.putExtra(EXTRA_USER_REVIEW_GRANTED_CONSENT, mReviewGrantedConsentRequired);
                intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);

                // Start activity from the current foreground user to avoid creating a separate
@@ -259,11 +301,11 @@ public class MediaProjectionPermissionActivity extends Activity
        } catch (RemoteException e) {
            Log.e(TAG, "Error granting projection permission", e);
            setResult(RESULT_CANCELED);
            finish(RECORD_CANCEL, /* projection= */ null);
        } finally {
            if (mDialog != null) {
                mDialog.dismiss();
            }
            finish();
        }
    }

@@ -271,22 +313,22 @@ public class MediaProjectionPermissionActivity extends Activity
        return UserHandle.getUserHandleForUid(getLaunchedFromUid());
    }

    private IMediaProjection createProjection(int uid, String packageName) throws RemoteException {
        return mService.createProjection(uid, packageName,
                MediaProjectionManager.TYPE_SCREEN_CAPTURE, false /* permanentGrant */);
    @Override
    public void finish() {
        // Default to cancelling recording when user needs to review consent.
        finish(RECORD_CANCEL, /* projection= */ null);
    }

    private Intent getMediaProjectionIntent(int uid, String packageName)
            throws RemoteException {
        IMediaProjection projection = createProjection(uid, packageName);
        Intent intent = new Intent();
        intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION, projection.asBinder());
        return intent;
    private void finish(@ReviewGrantedConsentResult int consentResult,
            @Nullable IMediaProjection projection) {
        MediaProjectionServiceHelper.setReviewedConsentIfNeeded(
                consentResult, mReviewGrantedConsentRequired, projection);
        super.finish();
    }

    private void onDialogDismissedOrCancelled(DialogInterface dialogInterface) {
        if (!isFinishing()) {
            finish();
            finish(RECORD_CANCEL, /* projection= */ null);
        }
    }

+93 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.media

import android.content.Context
import android.media.projection.IMediaProjection
import android.media.projection.IMediaProjectionManager
import android.media.projection.MediaProjectionManager
import android.media.projection.ReviewGrantedConsentResult
import android.os.RemoteException
import android.os.ServiceManager
import android.util.Log

/**
 * Helper class that handles the media projection service related actions. It simplifies invoking
 * the MediaProjectionManagerService and updating the permission consent.
 */
class MediaProjectionServiceHelper {
    companion object {
        private const val TAG = "MediaProjectionServiceHelper"
        private val service =
            IMediaProjectionManager.Stub.asInterface(
                ServiceManager.getService(Context.MEDIA_PROJECTION_SERVICE)
            )

        @JvmStatic
        @Throws(RemoteException::class)
        fun hasProjectionPermission(uid: Int, packageName: String) =
            service.hasProjectionPermission(uid, packageName)

        @JvmStatic
        @Throws(RemoteException::class)
        fun createOrReuseProjection(
            uid: Int,
            packageName: String,
            reviewGrantedConsentRequired: Boolean
        ): IMediaProjection {
            val existingProjection =
                if (reviewGrantedConsentRequired) service.getProjection(uid, packageName) else null
            return existingProjection
                ?: service.createProjection(
                    uid,
                    packageName,
                    MediaProjectionManager.TYPE_SCREEN_CAPTURE,
                    false /* permanentGrant */
                )
        }

        /**
         * This method is called when a host app reuses the consent token. If the token is being
         * used more than once, ask the user to review their consent and send the reviewed result.
         *
         * @param consentResult consent result to update
         * @param reviewGrantedConsentRequired if user must review already-granted consent that the
         *   host app is attempting to reuse
         * @param projection projection token associated with the consent result, or null if the
         *   result is for cancelling.
         */
        @JvmStatic
        fun setReviewedConsentIfNeeded(
            @ReviewGrantedConsentResult consentResult: Int,
            reviewGrantedConsentRequired: Boolean,
            projection: IMediaProjection?
        ) {
            // Only send the result to the server, when the user needed to review the re-used
            // consent token.
            if (
                reviewGrantedConsentRequired && consentResult != ReviewGrantedConsentResult.UNKNOWN
            ) {
                try {
                    service.setUserReviewGrantedConsentResult(consentResult, projection)
                } catch (e: RemoteException) {
                    // If we are unable to pass back the result, capture continues with blank frames
                    Log.e(TAG, "Unable to set required consent result for token re-use", e)
                }
            }
        }
    }
}
+6 −1
Original line number Diff line number Diff line
@@ -43,6 +43,7 @@ open class BaseScreenSharePermissionDialog(
) : SystemUIDialog(context), AdapterView.OnItemSelectedListener {
    private lateinit var dialogTitle: TextView
    private lateinit var startButton: TextView
    private lateinit var cancelButton: TextView
    private lateinit var warning: TextView
    private lateinit var screenShareModeSpinner: Spinner
    var selectedScreenShareOption: ScreenShareOption = screenShareOptions.first()
@@ -57,7 +58,7 @@ open class BaseScreenSharePermissionDialog(
        dialogTitle = findViewById(R.id.screen_share_dialog_title)
        warning = findViewById(R.id.text_warning)
        startButton = findViewById(R.id.button_start)
        findViewById<TextView>(R.id.button_cancel).setOnClickListener { dismiss() }
        cancelButton = findViewById(R.id.button_cancel)
        updateIcon()
        initScreenShareOptions()
        createOptionsView(getOptionsViewLayoutId())
@@ -117,6 +118,10 @@ open class BaseScreenSharePermissionDialog(
        startButton.setOnClickListener(listener)
    }

    protected fun setCancelButtonOnClickListener(listener: View.OnClickListener?) {
        cancelButton.setOnClickListener(listener)
    }

    // Create additional options that is shown under the share mode spinner
    // Eg. the audio and tap toggles in SysUI Recorder
    @LayoutRes protected open fun getOptionsViewLayoutId(): Int? = null
+5 −0
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import com.android.systemui.R
class MediaProjectionPermissionDialog(
    context: Context?,
    private val onStartRecordingClicked: Runnable,
    private val onCancelClicked: Runnable,
    private val appName: String?
) : BaseScreenSharePermissionDialog(context, createOptionList(appName), appName) {
    override fun onCreate(savedInstanceState: Bundle?) {
@@ -39,6 +40,10 @@ class MediaProjectionPermissionDialog(
            onStartRecordingClicked.run()
            dismiss()
        }
        setCancelButtonOnClickListener {
            onCancelClicked.run()
            dismiss()
        }
    }

    companion object {