diff --git a/core/api/system-current.txt b/core/api/system-current.txt index ab3feeb9cd17b..1f049618e8608 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -11199,6 +11199,7 @@ package android.service.games { method public void onTransientSystemBarVisibilityFromRevealGestureChanged(boolean); method @RequiresPermission(android.Manifest.permission.MANAGE_GAME_ACTIVITY) public final boolean restartGame(); method public void setTaskOverlayView(@NonNull android.view.View, @NonNull android.view.ViewGroup.LayoutParams); + method @RequiresPermission(android.Manifest.permission.MANAGE_GAME_ACTIVITY) public final void startActivityFromGameSessionForResult(@NonNull android.content.Intent, @Nullable android.os.Bundle, @NonNull java.util.concurrent.Executor, @NonNull android.service.games.GameSessionActivityCallback); method public void takeScreenshot(@NonNull java.util.concurrent.Executor, @NonNull android.service.games.GameSession.ScreenshotCallback); } @@ -11208,6 +11209,11 @@ package android.service.games { field public static final int ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR = 0; // 0x0 } + public interface GameSessionActivityCallback { + method public void onActivityResult(int, @Nullable android.content.Intent); + method public default void onActivityStartFailed(@NonNull Throwable); + } + public abstract class GameSessionService extends android.app.Service { ctor public GameSessionService(); method @Nullable public final android.os.IBinder onBind(@Nullable android.content.Intent); diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 90c56ae633743..8935022639e40 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -5503,6 +5503,17 @@ public class Activity extends ContextThemeWrapper */ public void startActivityAsCaller(Intent intent, @Nullable Bundle options, IBinder permissionToken, boolean ignoreTargetSecurity, int userId) { + startActivityAsCaller(intent, options, permissionToken, ignoreTargetSecurity, userId, -1); + } + + /** + * @see #startActivityAsCaller(Intent, Bundle, IBinder, boolean, int) + * @param requestCode The request code used for returning a result or -1 if no result should be + * returned. + * @hide + */ + public void startActivityAsCaller(Intent intent, @Nullable Bundle options, + IBinder permissionToken, boolean ignoreTargetSecurity, int userId, int requestCode) { if (mParent != null) { throw new RuntimeException("Can't be called from a child"); } @@ -5510,11 +5521,11 @@ public class Activity extends ContextThemeWrapper Instrumentation.ActivityResult ar = mInstrumentation.execStartActivityAsCaller( this, mMainThread.getApplicationThread(), mToken, this, - intent, -1, options, permissionToken, ignoreTargetSecurity, userId); + intent, requestCode, options, permissionToken, ignoreTargetSecurity, + userId); if (ar != null) { mMainThread.sendActivityResult( - mToken, mEmbeddedID, -1, ar.getResultCode(), - ar.getResultData()); + mToken, mEmbeddedID, requestCode, ar.getResultCode(), ar.getResultData()); } cancelInputsAndStartExitTransition(options); } diff --git a/core/java/android/app/IActivityTaskManager.aidl b/core/java/android/app/IActivityTaskManager.aidl index 0801b2481f0c6..c5add66e0a142 100644 --- a/core/java/android/app/IActivityTaskManager.aidl +++ b/core/java/android/app/IActivityTaskManager.aidl @@ -130,6 +130,9 @@ interface IActivityTaskManager { in ProfilerInfo profilerInfo, in Bundle options, int userId); int startAssistantActivity(in String callingPackage, in String callingFeatureId, int callingPid, int callingUid, in Intent intent, in String resolvedType, in Bundle options, int userId); + int startActivityFromGameSession(IApplicationThread caller, in String callingPackage, + in String callingFeatureId, int callingPid, int callingUid, in Intent intent, + int taskId, int userId); void startRecentsActivity(in Intent intent, in long eventTime, in IRecentsAnimationRunner recentsAnimationRunner); int startActivityFromRecents(int taskId, in Bundle options); diff --git a/core/java/android/service/games/GameSession.java b/core/java/android/service/games/GameSession.java index 0987903c98090..468e087c941b3 100644 --- a/core/java/android/service/games/GameSession.java +++ b/core/java/android/service/games/GameSession.java @@ -20,15 +20,23 @@ import android.annotation.Hide; import android.annotation.IntDef; import android.annotation.MainThread; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; +import android.app.ActivityTaskManager; +import android.app.Instrumentation; +import android.content.ComponentName; import android.content.Context; +import android.content.Intent; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Rect; +import android.os.Binder; +import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.RemoteException; +import android.os.UserHandle; import android.util.Slog; import android.view.SurfaceControlViewHost; import android.view.View; @@ -41,6 +49,7 @@ import com.android.internal.util.function.pooled.PooledLambda; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Objects; import java.util.concurrent.Executor; /** @@ -120,6 +129,7 @@ public abstract class GameSession { private LifecycleState mLifecycleState = LifecycleState.INITIALIZED; private boolean mAreTransientInsetsVisibleDueToGesture = false; private IGameSessionController mGameSessionController; + private Context mContext; private int mTaskId; private GameSessionRootView mGameSessionRootView; private SurfaceControlViewHost mSurfaceControlViewHost; @@ -137,6 +147,7 @@ public abstract class GameSession { int heightPx) { mGameSessionController = gameSessionController; mTaskId = taskId; + mContext = context; mSurfaceControlViewHost = surfaceControlViewHost; mGameSessionRootView = new GameSessionRootView(context, mSurfaceControlViewHost); surfaceControlViewHost.setView(mGameSessionRootView, widthPx, heightPx); @@ -458,4 +469,67 @@ public abstract class GameSession { break; } } + + /** + * Launches an activity within the same activity stack as the {@link GameSession}. When the + * target activity exits, {@link GameSessionActivityCallback#onActivityResult(int, Intent)} will + * be invoked with the result code and result data directly from the target activity (in other + * words, the result code and data set via the target activity's + * {@link android.app.Activity#startActivityForResult} call). The caller is expected to handle + * the results that the target activity returns. + * + *

Any activity that an app would normally be able to start via {@link + * android.app.Activity#startActivityForResult} will be startable via this method. + * + *

Started activities may see a different calling package than the game session's package + * when calling {@link android.app.Activity#getCallingPackage()}. + * + *

If an exception is thrown while handling {@code intent}, + * {@link GameSessionActivityCallback#onActivityStartFailed(Throwable)} will be called instead + * of {@link GameSessionActivityCallback#onActivityResult(int, Intent)}. + * + * @param intent The intent to start. + * @param options Additional options for how the Activity should be started. See + * {@link android.app.Activity#startActivityForResult(Intent, int, Bundle)} for + * more details. This value may be null. + * @param executor Executor on which {@code callback} should be invoked. + * @param callback Callback to be invoked once the started activity has finished. + */ + @RequiresPermission(android.Manifest.permission.MANAGE_GAME_ACTIVITY) + public final void startActivityFromGameSessionForResult( + @NonNull Intent intent, @Nullable Bundle options, @NonNull Executor executor, + @NonNull GameSessionActivityCallback callback) { + Objects.requireNonNull(intent); + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + + AndroidFuture future = + new AndroidFuture() + .whenCompleteAsync((result, ex) -> { + if (ex != null) { + callback.onActivityStartFailed(ex); + return; + } + callback.onActivityResult(result.getResultCode(), result.getData()); + }, executor); + + final Intent trampolineIntent = new Intent(); + trampolineIntent.setComponent( + new ComponentName( + "android", "android.service.games.GameSessionTrampolineActivity")); + trampolineIntent.putExtra(GameSessionTrampolineActivity.INTENT_KEY, intent); + trampolineIntent.putExtra(GameSessionTrampolineActivity.OPTIONS_KEY, options); + trampolineIntent.putExtra( + GameSessionTrampolineActivity.FUTURE_KEY, future); + + try { + int result = ActivityTaskManager.getService().startActivityFromGameSession( + mContext.getIApplicationThread(), mContext.getPackageName(), "GameSession", + Binder.getCallingPid(), Binder.getCallingUid(), trampolineIntent, mTaskId, + UserHandle.myUserId()); + Instrumentation.checkStartActivityResult(result, trampolineIntent); + } catch (Throwable t) { + executor.execute(() -> callback.onActivityStartFailed(t)); + } + } } diff --git a/core/java/android/service/games/GameSessionActivityCallback.java b/core/java/android/service/games/GameSessionActivityCallback.java new file mode 100644 index 0000000000000..3b11df1fe644d --- /dev/null +++ b/core/java/android/service/games/GameSessionActivityCallback.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.games; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.content.Intent; +import android.os.Bundle; + +import java.util.concurrent.Executor; + +/** + * Callback invoked when an activity launched via + * {@link GameSession#startActivityFromGameSessionForResult(Intent, Bundle, Executor, + * GameSessionActivityCallback)}} has returned a result or failed to start. + * + * @hide + */ +@SystemApi +public interface GameSessionActivityCallback { + /** + * Callback invoked when an activity launched via + * {@link GameSession#startActivityFromGameSessionForResult(Intent, Bundle, Executor, + * GameSessionActivityCallback)}} has returned a result. + * + * @param resultCode The result code of the launched activity. See {@link + * android.app.Activity#setResult(int)}. + * @param data Any data returned by the launched activity. See {@link + * android.app.Activity#setResult(int, Intent)}. + */ + void onActivityResult(int resultCode, @Nullable Intent data); + + /** + * Callback invoked when a throwable was thrown when launching the {@link Intent} in + * {@link GameSession#startActivityFromGameSessionForResult(Intent, Bundle, Executor, + * GameSessionActivityCallback)}}. + */ + default void onActivityStartFailed(@NonNull Throwable t) {} +} diff --git a/core/java/android/service/games/GameSessionActivityResult.java b/core/java/android/service/games/GameSessionActivityResult.java new file mode 100644 index 0000000000000..a2ec6ada010c5 --- /dev/null +++ b/core/java/android/service/games/GameSessionActivityResult.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.games; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Intent; +import android.os.Parcel; +import android.os.Parcelable; + + +final class GameSessionActivityResult implements Parcelable { + + public static final Creator CREATOR = + new Creator() { + @Override + public GameSessionActivityResult createFromParcel(Parcel in) { + int resultCode = in.readInt(); + Intent data = in.readParcelable(Intent.class.getClassLoader(), Intent.class); + return new GameSessionActivityResult(resultCode, data); + } + + @Override + public GameSessionActivityResult[] newArray(int size) { + return new GameSessionActivityResult[size]; + } + }; + + private final int mResultCode; + @Nullable + private final Intent mData; + + GameSessionActivityResult(int resultCode, @Nullable Intent data) { + mResultCode = resultCode; + mData = data; + } + + int getResultCode() { + return mResultCode; + } + + @Nullable + Intent getData() { + return mData; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mResultCode); + dest.writeParcelable(mData, flags); + } +} diff --git a/core/java/android/service/games/GameSessionTrampolineActivity.java b/core/java/android/service/games/GameSessionTrampolineActivity.java new file mode 100644 index 0000000000000..ddea098680eae --- /dev/null +++ b/core/java/android/service/games/GameSessionTrampolineActivity.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.games; + +import android.annotation.Nullable; +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.util.Slog; + +import com.android.internal.infra.AndroidFuture; + +import java.util.concurrent.Executor; + +/** + * Trampoline activity that enables the + * {@link GameSession#startActivityFromGameSessionForResult(Intent, Bundle, Executor, + * GameSessionActivityCallback)} API by reusing existing activity result infrastructure in the + * {@link Activity} class. This activity forwards activity results back to the calling + * {@link GameSession} via {@link AndroidFuture}. + * + * @hide + */ +public final class GameSessionTrampolineActivity extends Activity { + private static final String TAG = "GameSessionTrampoline"; + private static final int REQUEST_CODE = 1; + + static final String FUTURE_KEY = "GameSessionTrampolineActivity.future"; + static final String INTENT_KEY = "GameSessionTrampolineActivity.intent"; + static final String OPTIONS_KEY = "GameSessionTrampolineActivity.options"; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + try { + startActivityAsCaller( + getIntent().getParcelableExtra(INTENT_KEY), + getIntent().getBundleExtra(OPTIONS_KEY), + null, + false, + getUserId(), + REQUEST_CODE); + } catch (Exception e) { + Slog.w(TAG, "Unable to launch activity from game session"); + AndroidFuture future = getIntent().getParcelableExtra( + FUTURE_KEY); + future.completeExceptionally(e); + finish(); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode != REQUEST_CODE) { + // Something went very wrong if we hit this code path, and we should bail. + throw new IllegalStateException("Unexpected request code: " + requestCode); + } + + AndroidFuture future = getIntent().getParcelableExtra( + FUTURE_KEY); + future.complete(new GameSessionActivityResult(resultCode, data)); + finish(); + } +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 82ebdc91cb511..506a0c09b7c9a 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -6560,6 +6560,13 @@ android:exported="false"> + + + diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 8d4ae908e79f0..ca4d7178fbccd 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -24,6 +24,7 @@ import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL; import static android.Manifest.permission.INTERNAL_SYSTEM_WINDOW; import static android.Manifest.permission.MANAGE_ACTIVITY_STACKS; import static android.Manifest.permission.MANAGE_ACTIVITY_TASKS; +import static android.Manifest.permission.MANAGE_GAME_ACTIVITY; import static android.Manifest.permission.READ_FRAME_BUFFER; import static android.Manifest.permission.REMOVE_TASKS; import static android.Manifest.permission.START_TASKS_FROM_RECENTS; @@ -1770,6 +1771,43 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } } + @Override + public int startActivityFromGameSession(IApplicationThread caller, String callingPackage, + String callingFeatureId, int callingPid, int callingUid, Intent intent, int taskId, + int userId) { + if (checkCallingPermission(MANAGE_GAME_ACTIVITY) != PERMISSION_GRANTED) { + final String msg = "Permission Denial: startActivityFromGameSession() from pid=" + + Binder.getCallingPid() + + ", uid=" + Binder.getCallingUid() + + " requires " + MANAGE_GAME_ACTIVITY; + Slog.w(TAG, msg); + throw new SecurityException(msg); + } + assertPackageMatchesCallingUid(callingPackage); + + final ActivityOptions activityOptions = ActivityOptions.makeBasic(); + activityOptions.setLaunchTaskId(taskId); + + userId = handleIncomingUser(callingPid, callingUid, userId, "startActivityFromGameSession"); + + final long origId = Binder.clearCallingIdentity(); + try { + return getActivityStartController() + .obtainStarter(intent, "startActivityFromGameSession") + .setCaller(caller) + .setCallingUid(callingUid) + .setCallingPid(callingPid) + .setCallingPackage(intent.getPackage()) + .setCallingFeatureId(callingFeatureId) + .setUserId(userId) + .setActivityOptions(activityOptions.toBundle()) + .setRealCallingUid(Binder.getCallingUid()) + .execute(); + } finally { + Binder.restoreCallingIdentity(origId); + } + } + @Override public BackNavigationInfo startBackNavigation() { mAmInternal.enforceCallingPermission(START_TASKS_FROM_RECENTS,