diff options
5 files changed, 209 insertions, 33 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt index 52d417140e04..0860c207ef20 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt @@ -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() diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java index ccddd1d359b7..e217e36d1051 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java @@ -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); + 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(); - 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); } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionServiceHelper.kt b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionServiceHelper.kt new file mode 100644 index 000000000000..9e616e2355e2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionServiceHelper.kt @@ -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) + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/BaseScreenSharePermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/BaseScreenSharePermissionDialog.kt index db0052a4d99e..f63bf07a5a3f 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/BaseScreenSharePermissionDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/BaseScreenSharePermissionDialog.kt @@ -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 diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/MediaProjectionPermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/MediaProjectionPermissionDialog.kt index c5a82ce110de..201557c03e48 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/MediaProjectionPermissionDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/MediaProjectionPermissionDialog.kt @@ -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 { |