summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Christian Göllner <chrisgollner@google.com> 2023-06-28 12:46:18 +0100
committer Christian Göllner <chrisgollner@google.com> 2023-06-28 13:26:36 +0100
commit62a6b1f25a2f219fd9f04268daed6e83e7b7aed2 (patch)
tree28002a1dd537ff720938846d8c6de4057ec541c1
parentb6ca21a64d6b8619513504d8ff08c6c29534c7e7 (diff)
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
-rw-r--r--media/java/android/media/projection/IMediaProjectionWatcherCallback.aidl13
-rw-r--r--media/java/android/media/projection/MediaProjectionManager.java24
-rw-r--r--services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java54
-rw-r--r--services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java70
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java59
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();