[MediaProjection] Re-show permission dialog when token reuse

Regain user's consent if a token is being reuse, and update the consent to the server accordingly

Fixes: 253218374
Test: manual
Change-Id: Ic4cdc1f903ee55ad8eef861811f2d896eb345b25
This commit is contained in:
Naomi Musgrave
2023-04-21 18:33:35 +00:00
parent 3d3fc71b21
commit 0a66fbb609
5 changed files with 210 additions and 34 deletions

View File

@@ -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()

View File

@@ -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,13 +91,23 @@ public class MediaProjectionPermissionActivity extends Activity
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
mPackageName = getCallingPackage();
IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE);
mService = IMediaProjectionManager.Stub.asInterface(b);
final Intent launchingIntent = getIntent();
mReviewGrantedConsentRequired = launchingIntent.getBooleanExtra(
EXTRA_USER_REVIEW_GRANTED_CONSENT, false);
mPackageName = getCallingPackage();
// 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();
return;
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();
@@ -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);
}
}

View File

@@ -0,0 +1,93 @@
/*
* 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)
}
}
}
}
}

View File

@@ -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

View File

@@ -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 {