diff options
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 2231ce14eea6..e46d34e81483 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 5703c429c32b..5a68c53b8f68 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 65e34e682724..398e470d9fda 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<IBinder, IBinder.DeathRecipient>(); - 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<IBinder, IMediaProjectionCallback>(); mWatcherCallbacks = new ArrayMap<IBinder, IMediaProjectionWatcherCallback>(); } @@ -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 c42928eba85f..bb8b986c6f61 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<ContentRecordingSession> 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 197ee92aa7eb..64330d89984e 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; @@ -761,6 +763,63 @@ public class WindowManagerServiceTests extends WindowTestsBase { } @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( LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING)).isTrue(); |