Add #onPictureInPictureRequested to Activity and ATM
Allows vendors to signal that an activity should enter
picture-in-picture if possible.
Additionally, removes the need for an Activity to go
through onPause to enter picture-in-picture. Apps can
now override this new API and enter PIP from there
instead of relying on #onUserLeaveHint.
Bug: 143365086
Test: atest FrameworksCoreTests:android.app.activity.ActivityThreadTest
Test: atest CtsWindowManagerDeviceTestCases:PinnedStackTests
Test: atest WmTests:ActivityTaskManagerServiceTests
Change-Id: Ib7ae9d1a7055ceed73e9643982033de9d4ad7350
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 45b4818..3c5dd6c 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -156,6 +156,8 @@
import android.app.admin.DevicePolicyCache;
import android.app.assist.AssistContent;
import android.app.assist.AssistStructure;
+import android.app.servertransaction.ClientTransaction;
+import android.app.servertransaction.EnterPipRequestedItem;
import android.app.usage.UsageStatsManagerInternal;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
@@ -4941,6 +4943,44 @@
}
}
+ /**
+ * Requests that an activity should enter picture-in-picture mode if possible.
+ */
+ @Override
+ public void requestPictureInPictureMode(IBinder token) throws RemoteException {
+ mAmInternal.enforceCallingPermission(Manifest.permission.MANAGE_ACTIVITY_STACKS,
+ "requestPictureInPictureMode");
+ final long origId = Binder.clearCallingIdentity();
+ try {
+ synchronized (mGlobalLock) {
+ final ActivityRecord activity = ActivityRecord.forTokenLocked(token);
+ if (activity == null) {
+ return;
+ }
+
+ final boolean canEnterPictureInPicture = activity.checkEnterPictureInPictureState(
+ "requestPictureInPictureMode", /* beforeStopping */ false);
+ if (!canEnterPictureInPicture) {
+ throw new IllegalStateException(
+ "Requested PIP on an activity that doesn't support it");
+ }
+
+ try {
+ final ClientTransaction transaction = ClientTransaction.obtain(
+ activity.app.getThread(),
+ activity.token);
+ transaction.addCallback(EnterPipRequestedItem.obtain());
+ getLifecycleManager().scheduleTransaction(transaction);
+ } catch (Exception e) {
+ Slog.w(TAG, "Failed to send enter pip requested item: "
+ + activity.intent.getComponent(), e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(origId);
+ }
+ }
+
void dumpLastANRLocked(PrintWriter pw) {
pw.println("ACTIVITY MANAGER LAST ANR (dumpsys activity lastanr)");
if (mLastANRState == null) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
index f1de6e9..438de78 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
@@ -20,20 +20,30 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.PictureInPictureParams;
+import android.app.servertransaction.ClientTransaction;
+import android.app.servertransaction.EnterPipRequestedItem;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.IBinder;
+import android.os.RemoteException;
import android.view.IDisplayWindowListener;
import android.view.WindowContainerTransaction;
@@ -42,6 +52,7 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.MockitoSession;
import java.util.ArrayList;
@@ -56,6 +67,9 @@
@RunWith(WindowTestRunner.class)
public class ActivityTaskManagerServiceTests extends ActivityTestsBase {
+ private final ArgumentCaptor<ClientTransaction> mClientTransactionCaptor =
+ ArgumentCaptor.forClass(ClientTransaction.class);
+
@Before
public void setUp() throws Exception {
doReturn(false).when(mService).isBooting();
@@ -78,6 +92,39 @@
}
@Test
+ public void testOnPictureInPictureRequested() throws RemoteException {
+ final ActivityStack stack = new StackBuilder(mRootActivityContainer).build();
+ final ActivityRecord activity = stack.getBottomMostTask().getTopNonFinishingActivity();
+ ClientLifecycleManager lifecycleManager = mService.getLifecycleManager();
+ doNothing().when(lifecycleManager).scheduleTransaction(any());
+ doReturn(true).when(activity).checkEnterPictureInPictureState(anyString(), anyBoolean());
+
+ mService.requestPictureInPictureMode(activity.token);
+
+ verify(lifecycleManager).scheduleTransaction(mClientTransactionCaptor.capture());
+ final ClientTransaction transaction = mClientTransactionCaptor.getValue();
+ // Check that only an enter pip request item callback was scheduled.
+ assertEquals(1, transaction.getCallbacks().size());
+ assertTrue(transaction.getCallbacks().get(0) instanceof EnterPipRequestedItem);
+ // Check the activity lifecycle state remains unchanged.
+ assertNull(transaction.getLifecycleStateRequest());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testOnPictureInPictureRequested_cannotEnterPip() throws RemoteException {
+ final ActivityStack stack = new StackBuilder(mRootActivityContainer).build();
+ final ActivityRecord activity = stack.getBottomMostTask().getTopNonFinishingActivity();
+ ClientLifecycleManager lifecycleManager = mService.getLifecycleManager();
+ doNothing().when(lifecycleManager).scheduleTransaction(any());
+ doReturn(false).when(activity).checkEnterPictureInPictureState(anyString(), anyBoolean());
+
+ mService.requestPictureInPictureMode(activity.token);
+
+ // Check enter no transactions with enter pip requests are made.
+ verify(lifecycleManager, times(0)).scheduleTransaction(any());
+ }
+
+ @Test
public void testTaskTransaction() {
removeGlobalMinSizeRestriction();
final ActivityStack stack = new StackBuilder(mRootActivityContainer)