diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 7141259d7dce7..90c37d1999e1d 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -6432,6 +6432,20 @@ public class Activity extends ContextThemeWrapper } } + /** + * Ensures the activity's result is immediately returned to the caller when {@link #finish()} + * is invoked + * + *

Should be invoked alongside {@link #setResult(int, Intent)}, so the provided results are + * in place before finishing. Must only be invoked during MediaProjection setup. + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.MANAGE_MEDIA_PROJECTION) + public final void setForceSendResultForMediaProjection() { + ActivityClient.getInstance().setForceSendResultForMediaProjection(mToken); + } + /** * Call this to set the result that your activity will return to its * caller. diff --git a/core/java/android/app/ActivityClient.java b/core/java/android/app/ActivityClient.java index 73678d9f2ddad..1d3b5e2aa4ee8 100644 --- a/core/java/android/app/ActivityClient.java +++ b/core/java/android/app/ActivityClient.java @@ -17,6 +17,7 @@ package android.app; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.content.ComponentName; import android.content.Intent; import android.content.res.Configuration; @@ -184,6 +185,15 @@ public class ActivityClient { } } + @RequiresPermission(android.Manifest.permission.MANAGE_MEDIA_PROJECTION) + void setForceSendResultForMediaProjection(IBinder token) { + try { + getActivityClientController().setForceSendResultForMediaProjection(token); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + public boolean isTopOfTask(IBinder token) { try { return getActivityClientController().isTopOfTask(token); diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl index 0138186974a67..7aeb2b26dd15e 100644 --- a/core/java/android/app/IActivityClientController.aidl +++ b/core/java/android/app/IActivityClientController.aidl @@ -67,6 +67,12 @@ interface IActivityClientController { boolean finishActivityAffinity(in IBinder token); /** Finish all activities that were started for result from the specified activity. */ void finishSubActivity(in IBinder token, in String resultWho, int requestCode); + /** + * Indicates that when the activity finsihes, the result should be immediately sent to the + * originating activity. Must only be invoked during MediaProjection setup. + */ + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_MEDIA_PROJECTION)") + void setForceSendResultForMediaProjection(in IBinder token); boolean isTopOfTask(in IBinder token); boolean willActivityBeVisible(in IBinder token); diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index 47aa58751900b..f53b51d705574 100644 --- a/services/core/java/com/android/server/wm/ActivityClientController.java +++ b/services/core/java/com/android/server/wm/ActivityClientController.java @@ -43,6 +43,7 @@ import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_N import static com.android.server.wm.ActivityTaskManagerService.TAG_SWITCH; import static com.android.server.wm.ActivityTaskManagerService.enforceNotIsolatedCaller; +import android.Manifest; import android.annotation.ColorInt; import android.annotation.NonNull; import android.annotation.Nullable; @@ -462,8 +463,8 @@ class ActivityClientController extends IActivityClientController.Stub { // Explicitly dismissing the activity so reset its relaunch flag. r.mRelaunchReason = RELAUNCH_REASON_NONE; } else { - r.finishIfPossible(resultCode, resultData, resultGrants, - "app-request", true /* oomAdj */); + r.finishIfPossible(resultCode, resultData, resultGrants, "app-request", + true /* oomAdj */); res = r.finishing; if (!res) { Slog.i(TAG, "Failed to finish by app-request"); @@ -524,6 +525,23 @@ class ActivityClientController extends IActivityClientController.Stub { } } + @Override + public void setForceSendResultForMediaProjection(IBinder token) { + // Require that this is invoked only during MediaProjection setup. + mService.mAmInternal.enforceCallingPermission( + Manifest.permission.MANAGE_MEDIA_PROJECTION, + "setForceSendResultForMediaProjection"); + + final ActivityRecord r; + synchronized (mGlobalLock) { + r = ActivityRecord.isInRootTaskLocked(token); + if (r == null || !r.isInHistory()) { + return; + } + r.setForceSendResultForMediaProjection(); + } + } + @Override public boolean isTopOfTask(IBinder token) { synchronized (mGlobalLock) { diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 0672559e0a9d6..3a103f19c41d0 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -595,6 +595,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // pre-NYC apps that don't have a sense of being resized. int mRelaunchReason = RELAUNCH_REASON_NONE; + private boolean mForceSendResultForMediaProjection = false; + TaskDescription taskDescription; // the recents information for this activity // The locusId associated with this activity, if set. @@ -3257,7 +3259,12 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mAtmService.mUgmInternal.grantUriPermissionUncheckedFromIntent(resultGrants, resultTo.getUriPermissionsLocked()); } - resultTo.addResultLocked(this, resultWho, requestCode, resultCode, resultData); + if (mForceSendResultForMediaProjection) { + resultTo.sendResult(this.getUid(), resultWho, requestCode, resultCode, + resultData, resultGrants, true /* forceSendForMediaProjection */); + } else { + resultTo.addResultLocked(this, resultWho, requestCode, resultCode, resultData); + } resultTo = null; } else if (DEBUG_RESULTS) { Slog.v(TAG_RESULTS, "No result destination from " + this); @@ -3451,6 +3458,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } } + void setForceSendResultForMediaProjection() { + mForceSendResultForMediaProjection = true; + } + private void prepareActivityHideTransitionAnimationIfOvarlay() { if (mTaskOverlay) { prepareActivityHideTransitionAnimation(); @@ -4541,6 +4552,12 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A void sendResult(int callingUid, String resultWho, int requestCode, int resultCode, Intent data, NeededUriGrants dataGrants) { + sendResult(callingUid, resultWho, requestCode, resultCode, data, dataGrants, + false /* forceSendForMediaProjection */); + } + + private void sendResult(int callingUid, String resultWho, int requestCode, int resultCode, + Intent data, NeededUriGrants dataGrants, boolean forceSendForMediaProjection) { if (callingUid > 0) { mAtmService.mUgmInternal.grantUriPermissionUncheckedFromIntent(dataGrants, getUriPermissionsLocked()); @@ -4549,8 +4566,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (DEBUG_RESULTS) { Slog.v(TAG, "Send activity result to " + this + " : who=" + resultWho + " req=" + requestCode - + " res=" + resultCode + " data=" + data); + + " res=" + resultCode + " data=" + data + + " forceSendForMediaProjection=" + forceSendForMediaProjection); } + if (isState(RESUMED) && attachedToProcess()) { try { final ArrayList list = new ArrayList(); @@ -4563,9 +4582,63 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } } + // Schedule sending results now for Media Projection setup. + if (forceSendForMediaProjection && attachedToProcess() && isState(STARTED, PAUSING, PAUSED, + STOPPING, STOPPED)) { + final ClientTransaction transaction = ClientTransaction.obtain(app.getThread(), token); + // Build result to be returned immediately. + transaction.addCallback(ActivityResultItem.obtain( + List.of(new ResultInfo(resultWho, requestCode, resultCode, data)))); + // When the activity result is delivered, the activity will transition to RESUMED. + // Since the activity is only resumed so the result can be immediately delivered, + // return it to its original lifecycle state. + ActivityLifecycleItem lifecycleItem = getLifecycleItemForCurrentStateForResult(); + if (lifecycleItem != null) { + transaction.setLifecycleStateRequest(lifecycleItem); + } else { + Slog.w(TAG, "Unable to get the lifecycle item for state " + mState + + " so couldn't immediately send result"); + } + try { + mAtmService.getLifecycleManager().scheduleTransaction(transaction); + } catch (RemoteException e) { + Slog.w(TAG, "Exception thrown sending result to " + this, e); + } + } + addResultLocked(null /* from */, resultWho, requestCode, resultCode, data); } + /** + * Provides a lifecycle item for the current stat. Only to be used when force sending an + * activity result (as part of MeidaProjection setup). Does not support the following states: + * {@link State#INITIALIZING}, {@link State#RESTARTING_PROCESS}, + * {@link State#FINISHING}, {@link State#DESTROYING}, {@link State#DESTROYED}. It does not make + * sense to force send a result to an activity in these states. Does not support + * {@link State#RESUMED} since a resumed activity will end in the resumed state after handling + * the result. + * + * @return an {@link ActivityLifecycleItem} for the current state, or {@code null} if the + * state is not valid. + */ + @Nullable + private ActivityLifecycleItem getLifecycleItemForCurrentStateForResult() { + switch (mState) { + case STARTED: + return StartActivityItem.obtain(null); + case PAUSING: + case PAUSED: + return PauseActivityItem.obtain(); + case STOPPING: + case STOPPED: + return StopActivityItem.obtain(configChangeFlags); + default: + // Do not send a result immediately if the activity is in state INITIALIZING, + // RESTARTING_PROCESS, FINISHING, DESTROYING, or DESTROYED. + return null; + } + } + private void addNewIntentLocked(ReferrerIntent intent) { if (newIntents == null) { newIntents = new ArrayList<>(); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 19f25c6bef9a5..03310510b62dc 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -1160,6 +1160,45 @@ public class ActivityRecordTests extends WindowTestsBase { assertTrue(lastTransition.allReady()); } + @Test + public void testFinishActivityIfPossible_sendResultImmediately() { + // Create activity representing the source of the activity result. + final ComponentName sourceComponent = ComponentName.createRelative( + DEFAULT_COMPONENT_PACKAGE_NAME, ".SourceActivity"); + final ComponentName targetComponent = ComponentName.createRelative( + sourceComponent.getPackageName(), ".TargetActivity"); + + final ActivityRecord sourceActivity = new ActivityBuilder(mWm.mAtmService) + .setComponent(sourceComponent) + .setLaunchMode(ActivityInfo.LAUNCH_SINGLE_INSTANCE) + .setCreateTask(true) + .build(); + sourceActivity.finishing = false; + sourceActivity.setState(STOPPED, "test"); + + final ActivityRecord targetActivity = new ActivityBuilder(mWm.mAtmService) + .setComponent(targetComponent) + .setTargetActivity(sourceComponent.getClassName()) + .setLaunchMode(ActivityInfo.LAUNCH_SINGLE_INSTANCE) + .setCreateTask(true) + .setOnTop(true) + .build(); + targetActivity.finishing = false; + targetActivity.setState(RESUMED, "test"); + targetActivity.resultTo = sourceActivity; + targetActivity.setForceSendResultForMediaProjection(); + + clearInvocations(mAtm.getLifecycleManager()); + + targetActivity.finishIfPossible(0, new Intent(), null, "test", false /* oomAdj */); + + try { + verify(mAtm.getLifecycleManager(), atLeastOnce()).scheduleTransaction( + any(ClientTransaction.class)); + } catch (RemoteException ignored) { + } + } + /** * Verify that complete finish request for non-finishing activity is invalid. */