[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:
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user