From 62a6b1f25a2f219fd9f04268daed6e83e7b7aed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6llner?= Date: Wed, 28 Jun 2023 12:46:18 +0100 Subject: [PATCH] Partial Screen Sharing: Task Switcher - Add method exposing session to callback Adds a new method to MediaProjectionManager#Callback to inform when the ContentRecordingSession was set. This is needed for consumers that require more information about the recording session, such as the content that is being recorded (screen vs task). Bug: 286201261 Test: atest WmTests:WindowManagerServiceTests Test: atest FrameworksServicesTests:MediaProjectionManagerServiceTest Change-Id: Ie354d62dbc3896bd42079f2e5b5fb82cdb12706e --- .../IMediaProjectionWatcherCallback.aidl | 13 ++++ .../projection/MediaProjectionManager.java | 24 +++++++ .../MediaProjectionManagerService.java | 54 ++++++++++++-- .../MediaProjectionManagerServiceTest.java | 70 +++++++++++++++++++ .../server/wm/WindowManagerServiceTests.java | 59 ++++++++++++++++ 5 files changed, 216 insertions(+), 4 deletions(-) diff --git a/media/java/android/media/projection/IMediaProjectionWatcherCallback.aidl b/media/java/android/media/projection/IMediaProjectionWatcherCallback.aidl index 2231ce14eea64..e46d34e814836 100644 --- a/media/java/android/media/projection/IMediaProjectionWatcherCallback.aidl +++ b/media/java/android/media/projection/IMediaProjectionWatcherCallback.aidl @@ -17,9 +17,22 @@ package android.media.projection; import android.media.projection.MediaProjectionInfo; +import android.view.ContentRecordingSession; /** {@hide} */ oneway interface IMediaProjectionWatcherCallback { void onStart(in MediaProjectionInfo info); void onStop(in MediaProjectionInfo info); + /** + * Called when the {@link ContentRecordingSession} was set for the current media + * projection. + * + * @param info always present and contains information about the media projection host. + * @param session the recording session for the current media projection. Can be + * {@code null} when the recording will stop. + */ + void onRecordingSessionSet( + in MediaProjectionInfo info, + in @nullable ContentRecordingSession session + ); } diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java index 5703c429c32b4..5a68c53b8f68c 100644 --- a/media/java/android/media/projection/MediaProjectionManager.java +++ b/media/java/android/media/projection/MediaProjectionManager.java @@ -29,6 +29,7 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.util.ArrayMap; import android.util.Log; +import android.view.ContentRecordingSession; import android.view.Surface; import java.util.Map; @@ -300,7 +301,22 @@ public final class MediaProjectionManager { /** @hide */ public static abstract class Callback { public abstract void onStart(MediaProjectionInfo info); + public abstract void onStop(MediaProjectionInfo info); + + /** + * Called when the {@link ContentRecordingSession} was set for the current media + * projection. + * + * @param info always present and contains information about the media projection host. + * @param session the recording session for the current media projection. Can be + * {@code null} when the recording will stop. + */ + public void onRecordingSessionSet( + @NonNull MediaProjectionInfo info, + @Nullable ContentRecordingSession session + ) { + } } /** @hide */ @@ -335,5 +351,13 @@ public final class MediaProjectionManager { } }); } + + @Override + public void onRecordingSessionSet( + @NonNull final MediaProjectionInfo info, + @Nullable final ContentRecordingSession session + ) { + mHandler.post(() -> mCallback.onRecordingSessionSet(info, session)); + } } } 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 65e34e6827249..398e470d9fdaa 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -147,7 +147,7 @@ public final class MediaProjectionManagerService extends SystemService mInjector = injector; mClock = injector.createClock(); mDeathEaters = new ArrayMap(); - mCallbackDelegate = new CallbackDelegate(); + mCallbackDelegate = new CallbackDelegate(injector.createCallbackLooper()); mAppOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE); mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); mPackageManager = mContext.getPackageManager(); @@ -182,6 +182,11 @@ public final class MediaProjectionManagerService extends SystemService Clock createClock() { return SystemClock::uptimeMillis; } + + /** Creates the {@link Looper} to be used when notifying callbacks. */ + Looper createCallbackLooper() { + return Looper.getMainLooper(); + } } @Override @@ -268,7 +273,8 @@ public final class MediaProjectionManagerService extends SystemService dispatchStop(projection); } - private void addCallback(final IMediaProjectionWatcherCallback callback) { + @VisibleForTesting + void addCallback(final IMediaProjectionWatcherCallback callback) { IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() { @Override public void binderDied() { @@ -315,6 +321,12 @@ public final class MediaProjectionManagerService extends SystemService mCallbackDelegate.dispatchStop(projection); } + private void dispatchSessionSet( + @NonNull MediaProjectionInfo projectionInfo, + @Nullable ContentRecordingSession session) { + mCallbackDelegate.dispatchSession(projectionInfo, session); + } + /** * Returns {@code true} when updating the current mirroring session on WM succeeded, and * {@code false} otherwise. @@ -335,6 +347,7 @@ public final class MediaProjectionManagerService extends SystemService if (mProjectionGrant != null) { // Cache the session details. mProjectionGrant.mSession = incomingSession; + dispatchSessionSet(mProjectionGrant.getProjectionInfo(), incomingSession); } return true; } @@ -1155,8 +1168,8 @@ public final class MediaProjectionManagerService extends SystemService private Handler mHandler; private final Object mLock = new Object(); - public CallbackDelegate() { - mHandler = new Handler(Looper.getMainLooper(), null, true /*async*/); + CallbackDelegate(Looper callbackLooper) { + mHandler = new Handler(callbackLooper, null, true /*async*/); mClientCallbacks = new ArrayMap(); mWatcherCallbacks = new ArrayMap(); } @@ -1219,6 +1232,16 @@ public final class MediaProjectionManagerService extends SystemService } } + public void dispatchSession( + @NonNull MediaProjectionInfo projectionInfo, + @Nullable ContentRecordingSession session) { + synchronized (mLock) { + for (IMediaProjectionWatcherCallback callback : mWatcherCallbacks.values()) { + mHandler.post(new WatcherSessionCallback(callback, projectionInfo, session)); + } + } + } + public void dispatchResize(MediaProjection projection, int width, int height) { if (projection == null) { Slog.e(TAG, @@ -1335,6 +1358,29 @@ public final class MediaProjectionManagerService extends SystemService } } + private static final class WatcherSessionCallback implements Runnable { + private final IMediaProjectionWatcherCallback mCallback; + private final MediaProjectionInfo mProjectionInfo; + private final ContentRecordingSession mSession; + + WatcherSessionCallback( + @NonNull IMediaProjectionWatcherCallback callback, + @NonNull MediaProjectionInfo projectionInfo, + @Nullable ContentRecordingSession session) { + mCallback = callback; + mProjectionInfo = projectionInfo; + mSession = session; + } + + @Override + public void run() { + try { + mCallback.onRecordingSessionSet(mProjectionInfo, mSession); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to notify content recording session changed", e); + } + } + } private static String typeToString(int type) { switch (type) { 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 c42928eba85f0..bb8b986c6f61d 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 @@ -38,6 +38,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static org.testng.Assert.assertThrows; import android.app.ActivityManagerInternal; @@ -49,10 +50,14 @@ import android.content.pm.PackageManager.ApplicationInfoFlags; import android.content.pm.PackageManager.NameNotFoundException; import android.media.projection.IMediaProjection; import android.media.projection.IMediaProjectionCallback; +import android.media.projection.IMediaProjectionWatcherCallback; import android.media.projection.ReviewGrantedConsentResult; +import android.os.Binder; import android.os.IBinder; +import android.os.Looper; import android.os.RemoteException; import android.os.UserHandle; +import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; import android.view.ContentRecordingSession; @@ -86,6 +91,7 @@ public class MediaProjectionManagerServiceTest { private static final int UID = 10; private static final String PACKAGE_NAME = "test.package"; private final ApplicationInfo mAppInfo = new ApplicationInfo(); + private final TestLooper mTestLooper = new TestLooper(); private static final ContentRecordingSession DISPLAY_SESSION = ContentRecordingSession.createDisplaySession(DEFAULT_DISPLAY); // Callback registered by an app on a MediaProjection instance. @@ -110,6 +116,14 @@ public class MediaProjectionManagerServiceTest { } }; + private final MediaProjectionManagerService.Injector mTestLooperInjector = + new MediaProjectionManagerService.Injector() { + @Override + Looper createCallbackLooper() { + return mTestLooper.getLooper(); + } + }; + private Context mContext; private MediaProjectionManagerService mService; private OffsettableClock mClock; @@ -122,12 +136,15 @@ public class MediaProjectionManagerServiceTest { private WindowManagerInternal mWindowManagerInternal; @Mock private PackageManager mPackageManager; + @Mock + private IMediaProjectionWatcherCallback mWatcherCallback; @Captor private ArgumentCaptor mSessionCaptor; @Before public void setup() throws Exception { MockitoAnnotations.initMocks(this); + when(mWatcherCallback.asBinder()).thenReturn(new Binder()); LocalServices.removeServiceForTest(ActivityManagerInternal.class); LocalServices.addService(ActivityManagerInternal.class, mAmInternal); @@ -671,6 +688,59 @@ public class MediaProjectionManagerServiceTest { assertThat(mService.isCurrentProjection(projection)).isTrue(); } + @Test + public void setContentRecordingSession_successful_notifiesListeners() + throws Exception { + mService.addCallback(mWatcherCallback); + MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions(); + projection.start(mIMediaProjectionCallback); + + doReturn(true).when(mWindowManagerInternal).setContentRecordingSession( + any(ContentRecordingSession.class)); + mService.setContentRecordingSession(DISPLAY_SESSION); + + verify(mWatcherCallback).onRecordingSessionSet( + projection.getProjectionInfo(), + DISPLAY_SESSION + ); + } + + @Test + public void setContentRecordingSession_notifiesListenersOnCallbackLooper() + throws Exception { + mService = new MediaProjectionManagerService(mContext, mTestLooperInjector); + mService.addCallback(mWatcherCallback); + MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions(); + projection.start(mIMediaProjectionCallback); + doReturn(true).when(mWindowManagerInternal).setContentRecordingSession( + any(ContentRecordingSession.class)); + + mService.setContentRecordingSession(DISPLAY_SESSION); + // Callback not notified yet, as test looper hasn't dispatched the message yet + verify(mWatcherCallback, never()).onRecordingSessionSet(any(), any()); + + mTestLooper.dispatchAll(); + // Message dispatched on test looper. Callback should now be notified. + verify(mWatcherCallback).onRecordingSessionSet( + projection.getProjectionInfo(), + DISPLAY_SESSION + ); + } + + @Test + public void setContentRecordingSession_failure_doesNotNotifyListeners() + throws Exception { + mService.addCallback(mWatcherCallback); + MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions(); + projection.start(mIMediaProjectionCallback); + + doReturn(false).when(mWindowManagerInternal).setContentRecordingSession( + any(ContentRecordingSession.class)); + mService.setContentRecordingSession(DISPLAY_SESSION); + + verify(mWatcherCallback, never()).onRecordingSessionSet(any(), any()); + } + private void verifySetSessionWithContent(@ContentRecordingSession.RecordContent int content) { verify(mWindowManagerInternal, atLeastOnce()).setContentRecordingSession( mSessionCaptor.capture()); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index 197ee92aa7eb0..64330d89984e1 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -81,6 +81,7 @@ import android.os.UserHandle; import android.platform.test.annotations.Presubmit; import android.util.DisplayMetrics; import android.util.MergedConfiguration; +import android.view.ContentRecordingSession; import android.view.IWindow; import android.view.IWindowSessionCallback; import android.view.InputChannel; @@ -101,6 +102,7 @@ import androidx.test.platform.app.InstrumentationRegistry; import com.android.compatibility.common.util.AdoptShellPermissionsRule; import com.android.internal.os.IResultReceiver; +import com.android.server.LocalServices; import org.junit.Rule; import org.junit.Test; @@ -760,6 +762,63 @@ public class WindowManagerServiceTests extends WindowTestsBase { assertThat(wct).isNull(); } + @Test + public void setContentRecordingSession_sessionNull_returnsTrue() { + WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class); + + boolean result = wmInternal.setContentRecordingSession(/* incomingSession= */ null); + + assertThat(result).isTrue(); + } + + @Test + public void setContentRecordingSession_sessionContentDisplay_returnsTrue() { + WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class); + ContentRecordingSession session = ContentRecordingSession.createDisplaySession( + DEFAULT_DISPLAY); + + boolean result = wmInternal.setContentRecordingSession(session); + + assertThat(result).isTrue(); + } + + @Test + public void setContentRecordingSession_sessionContentTask_noMatchingTask_returnsFalse() { + WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class); + IBinder launchCookie = new Binder(); + ContentRecordingSession session = ContentRecordingSession.createTaskSession(launchCookie); + + boolean result = wmInternal.setContentRecordingSession(session); + + assertThat(result).isFalse(); + } + + @Test + public void setContentRecordingSession_sessionContentTask_matchingTask_returnsTrue() { + WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class); + ActivityRecord activityRecord = createActivityRecord(createTask(mDefaultDisplay)); + ContentRecordingSession session = ContentRecordingSession.createTaskSession( + activityRecord.mLaunchCookie); + + boolean result = wmInternal.setContentRecordingSession(session); + + assertThat(result).isTrue(); + } + + @Test + public void setContentRecordingSession_matchingTask_mutatesSessionWithWindowContainerToken() { + WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class); + Task task = createTask(mDefaultDisplay); + ActivityRecord activityRecord = createActivityRecord(task); + ContentRecordingSession session = ContentRecordingSession.createTaskSession( + activityRecord.mLaunchCookie); + + wmInternal.setContentRecordingSession(session); + + assertThat(session.getTokenToRecord()).isEqualTo( + task.mRemoteToken.toWindowContainerToken().asBinder()); + } + @Test public void testisLetterboxBackgroundMultiColored() { assertThat(setupLetterboxConfigurationWithBackgroundType(