summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--media/java/android/media/projection/IMediaProjection.aidl7
-rw-r--r--media/java/android/media/projection/IMediaProjectionManager.aidl5
-rw-r--r--media/java/android/media/projection/MediaProjection.java14
-rw-r--r--media/tests/projection/src/android/media/projection/FakeIMediaProjection.java6
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/RecordingServiceTest.java7
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt4
-rw-r--r--packages/SystemUI/res/values/strings.xml3
-rw-r--r--packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionServiceHelper.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java19
-rw-r--r--packages/SystemUI/src/com/android/systemui/mediaprojection/permission/ScreenShareOption.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java28
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java29
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt84
-rw-r--r--services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java58
-rw-r--r--services/tests/PackageManagerServiceTests/appenumeration/src/com/android/server/pm/test/appenumeration/AppEnumerationInternalTests.java7
-rw-r--r--services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java94
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.