summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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();