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
This commit is contained in:
Christian Göllner
2023-06-28 12:46:18 +01:00
parent b6ca21a64d
commit 62a6b1f25a
5 changed files with 216 additions and 4 deletions

View File

@@ -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
);
}

View File

@@ -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));
}
}
}

View File

@@ -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) {

View File

@@ -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());

View File

@@ -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(