diff options
17 files changed, 292 insertions, 93 deletions
diff --git a/media/java/android/media/projection/IMediaProjection.aidl b/media/java/android/media/projection/IMediaProjection.aidl index 7a1cf925bc6d..8ee966ddc377 100644 --- a/media/java/android/media/projection/IMediaProjection.aidl +++ b/media/java/android/media/projection/IMediaProjection.aidl @@ -56,6 +56,13 @@ interface IMediaProjection { + ".permission.MANAGE_MEDIA_PROJECTION)") int getTaskId(); + + /** + * Returns the displayId identifying the display to record. This only applies to full screen + * recording. + */ + int getDisplayId(); + /** * Updates the {@link LaunchCookie} identifying the task to record. */ diff --git a/media/java/android/media/projection/IMediaProjectionManager.aidl b/media/java/android/media/projection/IMediaProjectionManager.aidl index 3d927d36a369..b104972572b9 100644 --- a/media/java/android/media/projection/IMediaProjectionManager.aidl +++ b/media/java/android/media/projection/IMediaProjectionManager.aidl @@ -46,14 +46,15 @@ interface IMediaProjectionManager { boolean hasProjectionPermission(int processUid, String packageName); /** - * Returns a new {@link IMediaProjection} instance associated with the given package. + * Returns a new {@link IMediaProjection} instance associated with the given package for the + * given display id. * * @param processUid the process UID as returned by {@link android.os.Process.myUid()}. */ @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + ".permission.MANAGE_MEDIA_PROJECTION)") IMediaProjection createProjection(int processUid, String packageName, int type, - boolean permanentGrant); + boolean permanentGrant, int displayId); /** * Returns the current {@link IMediaProjection} instance associated with the given diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java index ef4c3ef0d321..4114f5359ace 100644 --- a/media/java/android/media/projection/MediaProjection.java +++ b/media/java/android/media/projection/MediaProjection.java @@ -18,6 +18,8 @@ package android.media.projection; import static android.view.Display.DEFAULT_DISPLAY; +import static com.android.media.projection.flags.Flags.mediaProjectionConnectedDisplay; + import android.annotation.NonNull; import android.annotation.Nullable; import android.app.compat.CompatChanges; @@ -85,13 +87,23 @@ public final class MediaProjection { public MediaProjection(Context context, IMediaProjection impl, DisplayManager displayManager) { mContext = context; mImpl = impl; + mDisplayManager = displayManager; + try { mImpl.start(new MediaProjectionCallback()); + + if (mediaProjectionConnectedDisplay()) { + int displayId = mImpl.getDisplayId(); + if (displayId != DEFAULT_DISPLAY) { + mDisplayId = displayId; + Log.v(TAG, "Created MediaProjection for display " + mDisplayId); + return; + } + } } catch (RemoteException e) { Log.e(TAG, "Content Recording: Failed to start media projection", e); throw new RuntimeException("Failed to start media projection", e); } - mDisplayManager = displayManager; final UserManager userManager = context.getSystemService(UserManager.class); mDisplayId = userManager.isVisibleBackgroundUsersSupported() diff --git a/media/tests/projection/src/android/media/projection/FakeIMediaProjection.java b/media/tests/projection/src/android/media/projection/FakeIMediaProjection.java index 6860c0bb2740..c9807e626429 100644 --- a/media/tests/projection/src/android/media/projection/FakeIMediaProjection.java +++ b/media/tests/projection/src/android/media/projection/FakeIMediaProjection.java @@ -22,6 +22,7 @@ import android.annotation.EnforcePermission; import android.app.ActivityOptions.LaunchCookie; import android.os.PermissionEnforcer; import android.os.RemoteException; +import android.view.Display; /** * The connection between MediaProjection and system server is represented by IMediaProjection; @@ -32,6 +33,7 @@ public final class FakeIMediaProjection extends IMediaProjection.Stub { boolean mIsStarted = false; LaunchCookie mLaunchCookie = null; IMediaProjectionCallback mIMediaProjectionCallback = null; + int mDisplayId = Display.DEFAULT_DISPLAY; FakeIMediaProjection(PermissionEnforcer enforcer) { super(enforcer); @@ -93,6 +95,10 @@ public final class FakeIMediaProjection extends IMediaProjection.Stub { return mTaskId; } + public int getDisplayId() { + return mDisplayId; + } + @Override @EnforcePermission(MANAGE_MEDIA_PROJECTION) public void setLaunchCookie(LaunchCookie launchCookie) throws RemoteException { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/RecordingServiceTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/RecordingServiceTest.java index bff3903e0114..a6a1d4a05dc7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/RecordingServiceTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/RecordingServiceTest.java @@ -310,6 +310,13 @@ public class RecordingServiceTest extends SysuiTestCase { verify(mNotificationManager).cancelAsUser(any(), anyInt(), any()); } + @Test + public void testSecondaryDisplayRecording() throws IOException { + Intent startIntent = + RecordingService.getStartIntent(mContext, 0, 0, false, 200, null); + assertEquals(startIntent.getIntExtra("extra_displayId", -1), 200); + } + private void assertUpdateState(boolean state) { // Then the state is set to not recording, and we cancel the notification // non SYSTEM user doesn't have the reference to the correct controller, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt index 7dae5ccd05c4..534c12cc0407 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.screenrecord import android.content.Intent +import android.hardware.display.DisplayManager import android.os.UserHandle import android.testing.TestableLooper import android.view.View @@ -89,6 +90,7 @@ class ScreenRecordPermissionDialogDelegateTest : SysuiTestCase() { mediaProjectionMetricsLogger, systemUIDialogFactory, context, + context.getSystemService(DisplayManager::class.java)!!, ) dialog = delegate.createDialog() } @@ -161,7 +163,7 @@ class ScreenRecordPermissionDialogDelegateTest : SysuiTestCase() { assertExtraPassedToAppSelector( extraKey = MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_UID, - value = TEST_HOST_UID + value = TEST_HOST_UID, ) } diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index c494e8525e0f..e485ef779bdc 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -289,7 +289,8 @@ <!-- Screen recording permission option for recording just a single app [CHAR LIMIT=50] --> <string name="screenrecord_permission_dialog_option_text_single_app">Record one app</string> <!-- Screen recording permission option for recording the whole screen [CHAR LIMIT=50] --> - <string name="screenrecord_permission_dialog_option_text_entire_screen">Record entire screen</string> + <string name="screenrecord_permission_dialog_option_text_entire_screen" >Record entire screen</string> + <string name="screenrecord_permission_dialog_option_text_entire_screen_for_display">Record entire screen: %s</string> <!-- Message reminding the user that sensitive information may be captured during a full screen recording for the updated dialog that includes partial screen sharing option [CHAR_LIMIT=350]--> <string name="screenrecord_permission_dialog_warning_entire_screen">When you’re recording your entire screen, anything shown on your screen is recorded. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string> <!-- Message reminding the user that sensitive information may be captured during a single app screen recording for the updated dialog that includes partial screen sharing option [CHAR_LIMIT=350]--> diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionServiceHelper.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionServiceHelper.kt index 0b19bab5c7c5..13a1f95213a8 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionServiceHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionServiceHelper.kt @@ -49,7 +49,8 @@ class MediaProjectionServiceHelper @Inject constructor() { fun createOrReuseProjection( uid: Int, packageName: String, - reviewGrantedConsentRequired: Boolean + reviewGrantedConsentRequired: Boolean, + displayId: Int, ): IMediaProjection { val existingProjection = if (reviewGrantedConsentRequired) service.getProjection(uid, packageName) else null @@ -58,7 +59,8 @@ class MediaProjectionServiceHelper @Inject constructor() { uid, packageName, MediaProjectionManager.TYPE_SCREEN_CAPTURE, - false /* permanentGrant */ + false /* permanentGrant */, + displayId, ) } @@ -76,7 +78,7 @@ class MediaProjectionServiceHelper @Inject constructor() { fun setReviewedConsentIfNeeded( @ReviewGrantedConsentResult consentResult: Int, reviewGrantedConsentRequired: Boolean, - projection: IMediaProjection? + projection: IMediaProjection?, ) { // Only send the result to the server, when the user needed to review the re-used // consent token. diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt index cdf8f06b5a23..32de56f93427 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt @@ -116,7 +116,7 @@ abstract class BaseMediaProjectionPermissionDialogDelegate<T : AlertDialog>( object : View.AccessibilityDelegate() { override fun onInitializeAccessibilityNodeInfo( host: View, - info: AccessibilityNodeInfo + info: AccessibilityNodeInfo, ) { info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK) super.onInitializeAccessibilityNodeInfo(host, info) @@ -169,14 +169,11 @@ abstract class BaseMediaProjectionPermissionDialogDelegate<T : AlertDialog>( } } -private class OptionsAdapter( - context: Context, - private val options: List<ScreenShareOption>, -) : +private class OptionsAdapter(context: Context, private val options: List<ScreenShareOption>) : ArrayAdapter<String>( context, R.layout.screen_share_dialog_spinner_text, - options.map { context.getString(it.spinnerText) } + options.map { context.getString(it.spinnerText, it.displayName) }, ) { override fun isEnabled(position: Int): Boolean { diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java index 212da9ffb9c5..c70cd0a3a11b 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java @@ -53,6 +53,7 @@ import android.text.BidiFormatter; import android.text.TextPaint; import android.text.TextUtils; import android.util.Log; +import android.view.Display; import android.view.Window; import com.android.systemui.flags.FeatureFlags; @@ -158,8 +159,11 @@ public class MediaProjectionPermissionActivity extends Activity { mUid, SessionCreationSource.APP); } final IMediaProjection projection = - MediaProjectionServiceHelper.createOrReuseProjection(mUid, mPackageName, - mReviewGrantedConsentRequired); + MediaProjectionServiceHelper.createOrReuseProjection( + mUid, + mPackageName, + mReviewGrantedConsentRequired, + Display.DEFAULT_DISPLAY); LaunchCookie launchCookie = launchingIntent.getParcelableExtra( MediaProjectionManager.EXTRA_LAUNCH_COOKIE, LaunchCookie.class); @@ -279,7 +283,9 @@ public class MediaProjectionPermissionActivity extends Activity { dialog -> { ScreenShareOption selectedOption = dialog.getSelectedScreenShareOption(); grantMediaProjectionPermission( - selectedOption.getMode(), hasCastingCapabilities); + selectedOption.getMode(), + hasCastingCapabilities, + selectedOption.getDisplayId()); }; Runnable onCancelClicked = () -> finish(RECORD_CANCEL, /* projection= */ null); if (hasCastingCapabilities) { @@ -368,10 +374,11 @@ public class MediaProjectionPermissionActivity extends Activity { } private void grantMediaProjectionPermission( - int screenShareMode, boolean hasCastingCapabilities) { + int screenShareMode, boolean hasCastingCapabilities, int displayId) { try { - IMediaProjection projection = MediaProjectionServiceHelper.createOrReuseProjection( - mUid, mPackageName, mReviewGrantedConsentRequired); + IMediaProjection projection = + MediaProjectionServiceHelper.createOrReuseProjection( + mUid, mPackageName, mReviewGrantedConsentRequired, displayId); if (screenShareMode == ENTIRE_SCREEN) { final Intent intent = new Intent(); setCommonIntentExtras(intent, hasCastingCapabilities, projection); diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/ScreenShareOption.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/ScreenShareOption.kt index ab921732ebf9..89383d0e9323 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/ScreenShareOption.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/ScreenShareOption.kt @@ -15,6 +15,7 @@ */ package com.android.systemui.mediaprojection.permission +import android.view.Display import androidx.annotation.IntDef import androidx.annotation.StringRes import kotlin.annotation.Retention @@ -31,5 +32,7 @@ data class ScreenShareOption( @StringRes val spinnerText: Int, @StringRes val warningText: Int, @StringRes val startButtonText: Int, + val displayId: Int = Display.DEFAULT_DISPLAY, val spinnerDisabledText: String? = null, + val displayName: String? = null, ) diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java index 6cc9ae4fb674..8c207d13d50e 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java @@ -36,6 +36,7 @@ import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; import android.util.Log; +import android.view.Display; import android.widget.Toast; import com.android.internal.annotations.VisibleForTesting; @@ -76,6 +77,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList private static final String EXTRA_AUDIO_SOURCE = "extra_useAudio"; private static final String EXTRA_SHOW_TAPS = "extra_showTaps"; private static final String EXTRA_CAPTURE_TARGET = "extra_captureTarget"; + private static final String EXTRA_DISPLAY_ID = "extra_displayId"; protected static final String ACTION_START = "com.android.systemui.screenrecord.START"; protected static final String ACTION_SHOW_START_NOTIF = @@ -141,6 +143,30 @@ public class RecordingService extends Service implements ScreenMediaRecorderList .putExtra(EXTRA_CAPTURE_TARGET, captureTarget); } + /** + * Get an intent to start the recording service. + * + * @param context Context from the requesting activity + * @param resultCode The result code from {@link android.app.Activity#onActivityResult(int, int, + * android.content.Intent)} + * @param audioSource The ordinal value of the audio source {@link + * com.android.systemui.screenrecord.ScreenRecordingAudioSource} + * @param showTaps True to make touches visible while recording + * @param captureTarget pass this parameter to capture a specific part instead of the full + * screen + * @param displayId The id of the display to record. + */ + public static Intent getStartIntent( + Context context, + int resultCode, + int audioSource, + boolean showTaps, + int displayId, + @Nullable MediaProjectionCaptureTarget captureTarget) { + return getStartIntent(context, resultCode, audioSource, showTaps, captureTarget) + .putExtra(EXTRA_DISPLAY_ID, displayId); + } + @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent == null) { @@ -174,6 +200,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList mOriginalShowTaps = Settings.System.getInt( getApplicationContext().getContentResolver(), Settings.System.SHOW_TOUCHES, 0) != 0; + int displayId = intent.getIntExtra(EXTRA_DISPLAY_ID, Display.DEFAULT_DISPLAY); setTapsVisible(mShowTaps); @@ -183,6 +210,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList currentUid, mAudioSource, captureTarget, + displayId, this, mScreenRecordingStartTimeStore ); diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java index 54da1b04aeb4..2ca0621635a7 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java @@ -50,8 +50,8 @@ import android.provider.MediaStore; import android.util.DisplayMetrics; import android.util.Log; import android.util.Size; +import android.view.Display; import android.view.Surface; -import android.view.WindowManager; import com.android.internal.R; import com.android.systemui.mediaprojection.MediaProjectionCaptureTarget; @@ -94,13 +94,18 @@ public class ScreenMediaRecorder extends MediaProjection.Callback { private final MediaProjectionCaptureTarget mCaptureRegion; private final ScreenRecordingStartTimeStore mScreenRecordingStartTimeStore; private final Handler mHandler; + private final int mDisplayId; private Context mContext; ScreenMediaRecorderListener mListener; - public ScreenMediaRecorder(Context context, Handler handler, - int uid, ScreenRecordingAudioSource audioSource, + public ScreenMediaRecorder( + Context context, + Handler handler, + int uid, + ScreenRecordingAudioSource audioSource, MediaProjectionCaptureTarget captureRegion, + int displayId, ScreenMediaRecorderListener listener, ScreenRecordingStartTimeStore screenRecordingStartTimeStore) { mContext = context; @@ -109,6 +114,7 @@ public class ScreenMediaRecorder extends MediaProjection.Callback { mCaptureRegion = captureRegion; mListener = listener; mAudioSource = audioSource; + mDisplayId = displayId; mScreenRecordingStartTimeStore = screenRecordingStartTimeStore; } @@ -117,9 +123,13 @@ public class ScreenMediaRecorder extends MediaProjection.Callback { IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE); IMediaProjectionManager mediaService = IMediaProjectionManager.Stub.asInterface(b); - IMediaProjection proj = null; - proj = mediaService.createProjection(mUid, mContext.getPackageName(), - MediaProjectionManager.TYPE_SCREEN_CAPTURE, false); + IMediaProjection proj = + mediaService.createProjection( + mUid, + mContext.getPackageName(), + MediaProjectionManager.TYPE_SCREEN_CAPTURE, + false, + mDisplayId); IMediaProjection projection = IMediaProjection.Stub.asInterface(proj.asBinder()); if (mCaptureRegion != null) { projection.setLaunchCookie(mCaptureRegion.getLaunchCookie()); @@ -146,9 +156,10 @@ public class ScreenMediaRecorder extends MediaProjection.Callback { // Set up video DisplayMetrics metrics = new DisplayMetrics(); - WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); - wm.getDefaultDisplay().getRealMetrics(metrics); - int refreshRate = (int) wm.getDefaultDisplay().getRefreshRate(); + DisplayManager dm = mContext.getSystemService(DisplayManager.class); + Display display = dm.getDisplay(mDisplayId); + display.getRealMetrics(metrics); + int refreshRate = (int) display.getRefreshRate(); int[] dimens = getSupportedSize(metrics.widthPixels, metrics.heightPixels, refreshRate); int width = dimens[0]; int height = dimens[1]; diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt index f3357ee53b7f..bdc58c1ceeb1 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt @@ -20,11 +20,14 @@ import android.app.Activity import android.app.PendingIntent import android.content.Context import android.content.Intent +import android.hardware.display.DisplayManager +import android.os.Build import android.os.Bundle import android.os.Handler import android.os.Looper import android.os.ResultReceiver import android.os.UserHandle +import android.view.Display import android.view.MotionEvent.ACTION_MOVE import android.view.View import android.view.View.GONE @@ -66,9 +69,10 @@ class ScreenRecordPermissionDialogDelegate( @ScreenShareMode defaultSelectedMode: Int, @StyleRes private val theme: Int, private val context: Context, + displayManager: DisplayManager, ) : BaseMediaProjectionPermissionDialogDelegate<SystemUIDialog>( - createOptionList(), + createOptionList(displayManager), appName = null, hostUid = hostUid, mediaProjectionMetricsLogger, @@ -88,6 +92,7 @@ class ScreenRecordPermissionDialogDelegate( mediaProjectionMetricsLogger: MediaProjectionMetricsLogger, systemUIDialogFactory: SystemUIDialog.Factory, @Application context: Context, + displayManager: DisplayManager, ) : this( hostUserHandle, hostUid, @@ -100,6 +105,7 @@ class ScreenRecordPermissionDialogDelegate( defaultSelectedMode = SINGLE_APP, theme = SystemUIDialog.DEFAULT_THEME, context, + displayManager, ) @AssistedFactory @@ -128,7 +134,7 @@ class ScreenRecordPermissionDialogDelegate( setStartButtonOnClickListener { v: View? -> onStartRecordingClicked?.run() if (selectedScreenShareOption.mode == ENTIRE_SCREEN) { - requestScreenCapture(/* captureTarget= */ null) + requestScreenCapture(/* captureTarget= */ null, selectedScreenShareOption.displayId) } if (selectedScreenShareOption.mode == SINGLE_APP) { val intent = Intent(dialog.context, MediaProjectionAppSelectorActivity::class.java) @@ -138,12 +144,12 @@ class ScreenRecordPermissionDialogDelegate( // the selected target to capture intent.putExtra( MediaProjectionAppSelectorActivity.EXTRA_CAPTURE_REGION_RESULT_RECEIVER, - CaptureTargetResultReceiver() + CaptureTargetResultReceiver(), ) intent.putExtra( MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE, - hostUserHandle + hostUserHandle, ) intent.putExtra(MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_UID, hostUid) intent.putExtra( @@ -178,7 +184,7 @@ class ScreenRecordPermissionDialogDelegate( ScreenRecordingAdapter( dialog.context, android.R.layout.simple_spinner_dropdown_item, - MODES + MODES, ) a.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) options.adapter = a @@ -191,7 +197,7 @@ class ScreenRecordPermissionDialogDelegate( object : View.AccessibilityDelegate() { override fun onInitializeAccessibilityNodeInfo( host: View, - info: AccessibilityNodeInfo + info: AccessibilityNodeInfo, ) { info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK) super.onInitializeAccessibilityNodeInfo(host, info) @@ -215,7 +221,10 @@ class ScreenRecordPermissionDialogDelegate( * @param captureTarget target to capture (could be e.g. a task) or null to record the whole * screen */ - private fun requestScreenCapture(captureTarget: MediaProjectionCaptureTarget?) { + private fun requestScreenCapture( + captureTarget: MediaProjectionCaptureTarget?, + displayId: Int = Display.DEFAULT_DISPLAY, + ) { val userContext = userContextProvider.userContext val showTaps = selectedScreenShareOption.mode != SINGLE_APP && tapsSwitch.isChecked val audioMode = @@ -230,28 +239,29 @@ class ScreenRecordPermissionDialogDelegate( Activity.RESULT_OK, audioMode.ordinal, showTaps, - captureTarget + displayId, + captureTarget, ), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, ) val stopIntent = PendingIntent.getService( userContext, RecordingService.REQUEST_CODE, RecordingService.getStopIntent(userContext), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, ) controller.startCountdown(DELAY_MS, INTERVAL_MS, startIntent, stopIntent) } - private inner class CaptureTargetResultReceiver() : + private inner class CaptureTargetResultReceiver : ResultReceiver(Handler(Looper.getMainLooper())) { override fun onReceiveResult(resultCode: Int, resultData: Bundle) { if (resultCode == Activity.RESULT_OK) { val captureTarget = resultData.getParcelable( MediaProjectionAppSelectorActivity.KEY_CAPTURE_TARGET, - MediaProjectionCaptureTarget::class.java + MediaProjectionCaptureTarget::class.java, ) // Start recording of the selected target @@ -265,12 +275,33 @@ class ScreenRecordPermissionDialogDelegate( listOf( ScreenRecordingAudioSource.INTERNAL, ScreenRecordingAudioSource.MIC, - ScreenRecordingAudioSource.MIC_AND_INTERNAL + ScreenRecordingAudioSource.MIC_AND_INTERNAL, ) private const val DELAY_MS: Long = 3000 private const val INTERVAL_MS: Long = 1000 - private fun createOptionList(): List<ScreenShareOption> { + private fun createOptionList(displayManager: DisplayManager): List<ScreenShareOption> { + if (!com.android.media.projection.flags.Flags.mediaProjectionConnectedDisplay()) { + return listOf( + ScreenShareOption( + SINGLE_APP, + R.string.screenrecord_permission_dialog_option_text_single_app, + R.string.screenrecord_permission_dialog_warning_single_app, + startButtonText = + R.string + .media_projection_entry_generic_permission_dialog_continue_single_app, + ), + ScreenShareOption( + ENTIRE_SCREEN, + R.string.screenrecord_permission_dialog_option_text_entire_screen, + R.string.screenrecord_permission_dialog_warning_entire_screen, + startButtonText = + R.string.screenrecord_permission_dialog_continue_entire_screen, + displayId = Display.DEFAULT_DISPLAY, + displayName = Build.MODEL, + ), + ) + } return listOf( ScreenShareOption( SINGLE_APP, @@ -282,12 +313,31 @@ class ScreenRecordPermissionDialogDelegate( ), ScreenShareOption( ENTIRE_SCREEN, - R.string.screenrecord_permission_dialog_option_text_entire_screen, + R.string.screenrecord_permission_dialog_option_text_entire_screen_for_display, R.string.screenrecord_permission_dialog_warning_entire_screen, startButtonText = R.string.screenrecord_permission_dialog_continue_entire_screen, - ) - ) + displayId = Display.DEFAULT_DISPLAY, + displayName = Build.MODEL, + ), + ) + + displayManager.displays + .filter { it.displayId != Display.DEFAULT_DISPLAY } + .map { + ScreenShareOption( + ENTIRE_SCREEN, + R.string + .screenrecord_permission_dialog_option_text_entire_screen_for_display, + warningText = + R.string + .media_projection_entry_app_permission_dialog_warning_entire_screen, + startButtonText = + R.string + .media_projection_entry_app_permission_dialog_continue_entire_screen, + displayId = it.displayId, + displayName = it.name, + ) + } } } } diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java index e0913ccbc7f7..436acba6e492 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -660,8 +660,13 @@ public final class MediaProjectionManagerService extends SystemService // TODO(b/261563516): Remove internal method and test aidl directly, here and elsewhere. @VisibleForTesting - MediaProjection createProjectionInternal(int uid, String packageName, int type, - boolean isPermanentGrant, UserHandle callingUser) { + MediaProjection createProjectionInternal( + int uid, + String packageName, + int type, + boolean isPermanentGrant, + UserHandle callingUser, + int displayId) { MediaProjection projection; ApplicationInfo ai; try { @@ -672,8 +677,14 @@ public final class MediaProjectionManagerService extends SystemService } final long callingToken = Binder.clearCallingIdentity(); try { - projection = new MediaProjection(type, uid, packageName, ai.targetSdkVersion, - ai.isPrivilegedApp()); + projection = + new MediaProjection( + type, + uid, + packageName, + ai.targetSdkVersion, + ai.isPrivilegedApp(), + displayId); if (isPermanentGrant) { mAppOps.setMode(AppOpsManager.OP_PROJECT_MEDIA, projection.uid, projection.packageName, AppOpsManager.MODE_ALLOWED); @@ -773,11 +784,16 @@ public final class MediaProjectionManagerService extends SystemService return hasPermission; } - @Override // Binder call - public IMediaProjection createProjection(int processUid, String packageName, int type, - boolean isPermanentGrant) { + // Binder call + @Override + public IMediaProjection createProjection( + int processUid, + String packageName, + int type, + boolean isPermanentGrant, + int displayId) { if (mContext.checkCallingPermission(MANAGE_MEDIA_PROJECTION) - != PackageManager.PERMISSION_GRANTED) { + != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to grant " + "projection permission"); } @@ -785,8 +801,8 @@ public final class MediaProjectionManagerService extends SystemService throw new IllegalArgumentException("package name must not be empty"); } final UserHandle callingUser = Binder.getCallingUserHandle(); - return createProjectionInternal(processUid, packageName, type, isPermanentGrant, - callingUser); + return createProjectionInternal( + processUid, packageName, type, isPermanentGrant, callingUser, displayId); } @Override // Binder call @@ -1074,6 +1090,10 @@ public final class MediaProjectionManagerService extends SystemService private final int mTargetSdkVersion; private final boolean mIsPrivileged; private final int mType; + // Values for tracking token validity. + // Timeout value to compare creation time against. + private final long mTimeoutMs = mDefaultTimeoutMs; + private final int mDisplayId; private IMediaProjectionCallback mCallback; private IBinder mToken; @@ -1082,9 +1102,6 @@ public final class MediaProjectionManagerService extends SystemService private int mTaskId = -1; private LaunchCookie mLaunchCookie = null; - // Values for tracking token validity. - // Timeout value to compare creation time against. - private long mTimeoutMs = mDefaultTimeoutMs; // Count of number of times IMediaProjection#start is invoked. private int mCountStarts = 0; // Set if MediaProjection#createVirtualDisplay has been invoked previously (it @@ -1093,8 +1110,13 @@ public final class MediaProjectionManagerService extends SystemService // The associated session details already sent to WindowManager. private ContentRecordingSession mSession; - MediaProjection(int type, int uid, String packageName, int targetSdkVersion, - boolean isPrivileged) { + MediaProjection( + int type, + int uid, + String packageName, + int targetSdkVersion, + boolean isPrivileged, + int displayId) { mType = type; this.uid = uid; this.packageName = packageName; @@ -1104,6 +1126,7 @@ public final class MediaProjectionManagerService extends SystemService mCreateTimeMs = mClock.uptimeMillis(); mActivityManagerInternal.notifyMediaProjectionEvent(uid, asBinder(), MEDIA_PROJECTION_TOKEN_EVENT_CREATED); + mDisplayId = displayId; } int getVirtualDisplayId() { @@ -1319,6 +1342,11 @@ public final class MediaProjectionManagerService extends SystemService return mTaskId; } + @Override // Binder call + public int getDisplayId() { + return mDisplayId; + } + @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_MEDIA_PROJECTION) @Override public boolean isValid() { diff --git a/services/tests/PackageManagerServiceTests/appenumeration/src/com/android/server/pm/test/appenumeration/AppEnumerationInternalTests.java b/services/tests/PackageManagerServiceTests/appenumeration/src/com/android/server/pm/test/appenumeration/AppEnumerationInternalTests.java index 4012d8e4af96..9f02b3fe4033 100644 --- a/services/tests/PackageManagerServiceTests/appenumeration/src/com/android/server/pm/test/appenumeration/AppEnumerationInternalTests.java +++ b/services/tests/PackageManagerServiceTests/appenumeration/src/com/android/server/pm/test/appenumeration/AppEnumerationInternalTests.java @@ -33,6 +33,7 @@ import android.media.projection.MediaProjectionManager; import android.os.Process; import android.os.ServiceManager; import android.os.UserHandle; +import android.view.Display; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; @@ -174,7 +175,8 @@ public class AppEnumerationInternalTests { ServiceManager.getService(MEDIA_PROJECTION_SERVICE)); assertThat(mediaProjectionManager.createProjection(0 /* uid */, TARGET_SHARED_USER, - MediaProjectionManager.TYPE_SCREEN_CAPTURE, false /* permanentGrant */)) + MediaProjectionManager.TYPE_SCREEN_CAPTURE, false /* permanentGrant */, + Display.DEFAULT_DISPLAY /* displayId */)) .isNotNull(); } @@ -187,7 +189,8 @@ public class AppEnumerationInternalTests { Assert.assertThrows(IllegalArgumentException.class, () -> mediaProjectionManager.createProjection(0 /* uid */, TARGET_SHARED_USER, - MediaProjectionManager.TYPE_SCREEN_CAPTURE, false /* permanentGrant */)); + MediaProjectionManager.TYPE_SCREEN_CAPTURE, false /* permanentGrant */, + Display.DEFAULT_DISPLAY /* displayId */)); } private static void installPackage(String apkPath, boolean forceQueryable) { diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java index b1d658cb1e86..73aec6375a03 100644 --- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java @@ -85,6 +85,7 @@ import android.provider.Settings; import android.testing.TestableContext; import android.view.ContentRecordingSession; import android.view.ContentRecordingSession.RecordContent; +import android.view.Display; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.FlakyTest; @@ -348,30 +349,42 @@ public class MediaProjectionManagerServiceTest { .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS) @Test public void testCreateProjection_keyguardLocked_RoleHeld() { - runWithRole(AssociationRequest.DEVICE_PROFILE_APP_STREAMING, () -> { - try { - mAppInfo.privateFlags |= PRIVATE_FLAG_PRIVILEGED; - doReturn(mAppInfo).when(mPackageManager).getApplicationInfoAsUser(anyString(), - any(ApplicationInfoFlags.class), any(UserHandle.class)); - MediaProjectionManagerService.MediaProjection projection = - mService.createProjectionInternal(Process.myUid(), - mContext.getPackageName(), - TYPE_MIRRORING, /* isPermanentGrant= */ false, UserHandle.CURRENT); - doReturn(true).when(mKeyguardManager).isKeyguardLocked(); - doReturn(PackageManager.PERMISSION_DENIED).when( - mPackageManager).checkPermission( - RECORD_SENSITIVE_CONTENT, projection.packageName); - - projection.start(mIMediaProjectionCallback); - projection.notifyVirtualDisplayCreated(10); - - // The projection was started because it was allowed to capture the keyguard. - assertWithMessage("Failed to run projection") - .that(mService.getActiveProjectionInfo()).isNotNull(); - } catch (NameNotFoundException e) { - throw new RuntimeException(e); - } - }); + runWithRole( + AssociationRequest.DEVICE_PROFILE_APP_STREAMING, + () -> { + try { + mAppInfo.privateFlags |= PRIVATE_FLAG_PRIVILEGED; + doReturn(mAppInfo) + .when(mPackageManager) + .getApplicationInfoAsUser( + anyString(), + any(ApplicationInfoFlags.class), + any(UserHandle.class)); + MediaProjectionManagerService.MediaProjection projection = + mService.createProjectionInternal( + Process.myUid(), + mContext.getPackageName(), + TYPE_MIRRORING, + /* isPermanentGrant= */ false, + UserHandle.CURRENT, + DEFAULT_DISPLAY); + doReturn(true).when(mKeyguardManager).isKeyguardLocked(); + doReturn(PackageManager.PERMISSION_DENIED) + .when(mPackageManager) + .checkPermission(RECORD_SENSITIVE_CONTENT, projection.packageName); + + projection.start(mIMediaProjectionCallback); + projection.notifyVirtualDisplayCreated(10); + + // The projection was started because it was allowed to capture the + // keyguard. + assertWithMessage("Failed to run projection") + .that(mService.getActiveProjectionInfo()) + .isNotNull(); + } catch (NameNotFoundException e) { + throw new RuntimeException(e); + } + }); } @EnableFlags(android.companion.virtualdevice.flags @@ -480,8 +493,13 @@ public class MediaProjectionManagerServiceTest { // We are allowed to create another projection. MediaProjectionManagerService.MediaProjection secondProjection = - mService.createProjectionInternal(UID + 10, PACKAGE_NAME + "foo", - TYPE_MIRRORING, /* isPermanentGrant= */ true, UserHandle.CURRENT); + mService.createProjectionInternal( + UID + 10, + PACKAGE_NAME + "foo", + TYPE_MIRRORING, + /* isPermanentGrant= */ true, + UserHandle.CURRENT, + Display.DEFAULT_DISPLAY); assertThat(secondProjection).isNotNull(); @@ -1246,6 +1264,13 @@ public class MediaProjectionManagerServiceTest { verify(mWatcherCallback, never()).onRecordingSessionSet(any(), any()); } + @Test + public void createProjectionForSecondaryDisplay() throws NameNotFoundException { + MediaProjectionManagerService.MediaProjection projection = + createProjectionPreconditions(mService, 200); + assertThat(projection.getDisplayId()).isEqualTo(200); + } + private void verifySetSessionWithContent(@RecordContent int content) { verify(mWindowManagerInternal, atLeastOnce()).setContentRecordingSession( mSessionCaptor.capture()); @@ -1255,12 +1280,21 @@ public class MediaProjectionManagerServiceTest { // Set up preconditions for creating a projection. private MediaProjectionManagerService.MediaProjection createProjectionPreconditions( - MediaProjectionManagerService service) - throws NameNotFoundException { + MediaProjectionManagerService service) throws NameNotFoundException { + return createProjectionPreconditions(service, Display.DEFAULT_DISPLAY); + } + + private MediaProjectionManagerService.MediaProjection createProjectionPreconditions( + MediaProjectionManagerService service, int displayId) throws NameNotFoundException { doReturn(mAppInfo).when(mPackageManager).getApplicationInfoAsUser(anyString(), any(ApplicationInfoFlags.class), any(UserHandle.class)); - return service.createProjectionInternal(UID, PACKAGE_NAME, - TYPE_MIRRORING, /* isPermanentGrant= */ false, UserHandle.CURRENT); + return service.createProjectionInternal( + UID, + PACKAGE_NAME, + TYPE_MIRRORING, + /* isPermanentGrant= */ false, + UserHandle.CURRENT, + displayId); } // Set up preconditions for starting a projection, with no foreground service requirements. |