Merge "Add GameSession APIs for starting activities."

This commit is contained in:
TreeHugger Robot
2022-02-04 09:25:47 +00:00
committed by Android (Google) Code Review
9 changed files with 346 additions and 3 deletions

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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.
*
* <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));
}
}
}

View File

@@ -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) {}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -6560,6 +6560,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">

View File

@@ -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,