Add GameSession APIs for starting activities.
This API allows the game service provider to start an activity in the existing game task and receive a result through the GameSessionActivityCallback instead of the result being sent to the game. For an example use case, in the Android S dashboard, the dashboard starts the YouTube-hosted live stream activity to handle YouTube live streaming and the Play Games-hosted dashboard activity to handle showing Play Games services data. The API allows the GameSession provider to respond to different activity results returned from the target activity. Test: manual e2e testing Bug: 208242220 Bug: 202414447 Bug: 202417255 CTS-Coverage-Bug: 206128693 Change-Id: I1eb017ade1ae60a713d85a70df23845498206df7
This commit is contained in:
@@ -11201,6 +11201,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);
|
||||
}
|
||||
|
||||
@@ -11210,6 +11211,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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
@@ -456,4 +467,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.
|
||||
*
|
||||
* <p>Any activity that an app would normally be able to start via {@link
|
||||
* android.app.Activity#startActivityForResult} will be startable via this method.
|
||||
*
|
||||
* <p>Started activities may see a different calling package than the game session's package
|
||||
* when calling {@link android.app.Activity#getCallingPackage()}.
|
||||
*
|
||||
* <p> 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<GameSessionActivityResult> future =
|
||||
new AndroidFuture<GameSessionActivityResult>()
|
||||
.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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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<GameSessionActivityResult> CREATOR =
|
||||
new Creator<GameSessionActivityResult>() {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -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<GameSessionActivityResult> 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<GameSessionActivityResult> future = getIntent().getParcelableExtra(
|
||||
FUTURE_KEY);
|
||||
future.complete(new GameSessionActivityResult(resultCode, data));
|
||||
finish();
|
||||
}
|
||||
}
|
||||
@@ -6546,6 +6546,13 @@
|
||||
android:exported="false">
|
||||
</activity>
|
||||
|
||||
<activity android:name="android.service.games.GameSessionTrampolineActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.MANAGE_GAME_ACTIVITY"
|
||||
android:theme="@style/Theme.Translucent.NoTitleBar">
|
||||
</activity>
|
||||
|
||||
<receiver android:name="com.android.server.BootReceiver"
|
||||
android:exported="true"
|
||||
android:systemUserOnly="true">
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user