diff options
| author | 2022-05-06 17:03:01 +0000 | |
|---|---|---|
| committer | 2022-06-07 16:23:12 +0000 | |
| commit | 12f5992e4df6b5948cf456306213e59c149f8c45 (patch) | |
| tree | 4010400682abc0c542e9a6325f10023374737fd5 | |
| parent | b8b70b0a046f443469d5c79f7afaf7aebe6b76d0 (diff) | |
[Partial Screenshare] introduce a hidden, permission-protected Activity API
Hidden Activity API allows a component holding permission
MANAGE_MEDIA_PROJECTION to declare that the result of setting up the
MediaProjection session should be returned immediately to the host VC app.
The host VC app will cycle through the resumed state when the activity
result is delivered to it, and return to its original state.
Manually tested with test MediaProjection app.
Test: atest WmTests:ActivityRecordTests
Bug: 230607871
Change-Id: Ic85fe8657b9e7f526bbc37a52011d16afa8aef2c
6 files changed, 164 insertions, 4 deletions
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 7141259d7dce..90c37d1999e1 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -6433,6 +6433,20 @@ public class Activity extends ContextThemeWrapper } /** + * Ensures the activity's result is immediately returned to the caller when {@link #finish()} + * is invoked + * + * <p>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 73678d9f2dda..1d3b5e2aa4ee 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 0138186974a6..7aeb2b26dd15 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 47aa58751900..f53b51d70557 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"); @@ -525,6 +526,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) { final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 398926f0ca85..48b1450043aa 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<ResultInfo> list = new ArrayList<ResultInfo>(); @@ -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 19f25c6bef9a..03310510b62d 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. */ |