Merge "Ensure GameSessionTrampolineActivity launches intent once." into tm-dev

This commit is contained in:
David Samuelson
2022-04-27 22:22:59 +00:00
committed by Android (Google) Code Review
6 changed files with 282 additions and 13 deletions

View File

@@ -25,7 +25,6 @@ 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;
@@ -511,14 +510,11 @@ public abstract class GameSession {
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);
final Intent trampolineIntent =
GameSessionTrampolineActivity.createIntent(
intent,
options,
future);
try {
int result = ActivityTaskManager.getService().startActivityFromGameSession(

View File

@@ -22,8 +22,12 @@ import android.content.Intent;
import android.os.Parcel;
import android.os.Parcelable;
import com.android.internal.annotations.VisibleForTesting;
final class GameSessionActivityResult implements Parcelable {
/** @hide */
@VisibleForTesting
public final class GameSessionActivityResult implements Parcelable {
public static final Creator<GameSessionActivityResult> CREATOR =
new Creator<GameSessionActivityResult>() {
@@ -44,17 +48,17 @@ final class GameSessionActivityResult implements Parcelable {
@Nullable
private final Intent mData;
GameSessionActivityResult(int resultCode, @Nullable Intent data) {
public GameSessionActivityResult(int resultCode, @Nullable Intent data) {
mResultCode = resultCode;
mData = data;
}
int getResultCode() {
public int getResultCode() {
return mResultCode;
}
@Nullable
Intent getData() {
public Intent getData() {
return mData;
}

View File

@@ -16,12 +16,15 @@
package android.service.games;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
import android.os.Bundle;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.infra.AndroidFuture;
import java.util.concurrent.Executor;
@@ -35,6 +38,7 @@ import java.util.concurrent.Executor;
*
* @hide
*/
@VisibleForTesting
public final class GameSessionTrampolineActivity extends Activity {
private static final String TAG = "GameSessionTrampoline";
private static final int REQUEST_CODE = 1;
@@ -42,11 +46,52 @@ public final class GameSessionTrampolineActivity extends Activity {
static final String FUTURE_KEY = "GameSessionTrampolineActivity.future";
static final String INTENT_KEY = "GameSessionTrampolineActivity.intent";
static final String OPTIONS_KEY = "GameSessionTrampolineActivity.options";
private static final String HAS_LAUNCHED_INTENT_KEY =
"GameSessionTrampolineActivity.hasLaunchedIntent";
private boolean mHasLaunchedIntent = false;
/**
* Create an {@link Intent} for the {@link GameSessionTrampolineActivity} with the given
* parameters.
*
* @param targetIntent the forwarded {@link Intent} that is associated with the Activity that
* will be launched by the {@link GameSessionTrampolineActivity}.
* @param options Activity options. See {@link #startActivity(Intent, Bundle)}.
* @param resultFuture the {@link AndroidFuture} that will complete with the activity results of
* {@code targetIntent} launched.
* @return the Intent that will launch the {@link GameSessionTrampolineActivity} with the given
* parameters.
* @hide
*/
@VisibleForTesting
public static Intent createIntent(
@NonNull Intent targetIntent,
@Nullable Bundle options,
@NonNull AndroidFuture<GameSessionActivityResult> resultFuture) {
final Intent trampolineIntent = new Intent();
trampolineIntent.setComponent(
new ComponentName(
"android", "android.service.games.GameSessionTrampolineActivity"));
trampolineIntent.putExtra(INTENT_KEY, targetIntent);
trampolineIntent.putExtra(OPTIONS_KEY, options);
trampolineIntent.putExtra(FUTURE_KEY, resultFuture);
return trampolineIntent;
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
mHasLaunchedIntent = savedInstanceState.getBoolean(HAS_LAUNCHED_INTENT_KEY);
}
if (mHasLaunchedIntent) {
return;
}
mHasLaunchedIntent = true;
try {
startActivityAsCaller(
getIntent().getParcelableExtra(INTENT_KEY),
@@ -63,6 +108,12 @@ public final class GameSessionTrampolineActivity extends Activity {
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(HAS_LAUNCHED_INTENT_KEY, mHasLaunchedIntent);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode != REQUEST_CODE) {

View File

@@ -42,6 +42,8 @@ android_test {
static_libs: [
"androidx.test.core",
"androidx.test.runner",
"androidx.test.espresso.core",
"androidx.test.espresso.contrib",
"androidx.test.ext.truth",
"frameworks-base-testutils",
"hamcrest-library",

View File

@@ -32,6 +32,8 @@
<uses-permission
android:name="android.permission.OVERRIDE_COMPAT_CHANGE_CONFIG_ON_RELEASE_BUILD"/>
<uses-permission android:name="android.permission.STATUS_BAR_SERVICE" />
<uses-permission android:name="android.permission.MANAGE_GAME_ACTIVITY" />
<uses-permission android:name="android.permission.SET_ALWAYS_FINISH" />
<!-- needed by MasterClearReceiverTest to display a system dialog -->
<uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW"/>
@@ -39,6 +41,8 @@
<application android:testOnly="true"
android:debuggable="true">
<uses-library android:name="android.test.runner" />
<activity
android:name="android.service.games.GameSessionTrampolineActivityTest$TestActivity" />
</application>
<instrumentation

View File

@@ -0,0 +1,212 @@
/*
* 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 static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isClickable;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static androidx.test.ext.truth.content.IntentSubject.assertThat;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.google.common.truth.Truth.assertThat;
import static org.hamcrest.Matchers.allOf;
import android.app.Activity;
import android.app.ActivityManager;
import android.content.Intent;
import android.os.Bundle;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
import android.testing.AndroidTestingRunner;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.test.espresso.NoActivityResumedException;
import androidx.test.filters.SmallTest;
import com.android.internal.infra.AndroidFuture;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.TimeUnit;
/**
* Unit tests for the {@link GameSessionTrampolineActivity}.
*/
@RunWith(AndroidTestingRunner.class)
@SmallTest
@Presubmit
public class GameSessionTrampolineActivityTest {
@Before
public void setUp() {
setAlwaysFinishActivities(false);
}
@After
public void tearDown() {
setAlwaysFinishActivities(false);
}
@Test
public void launch_launchesTargetActivity() {
AndroidFuture<GameSessionActivityResult> unusedResultFuture =
startTestActivityViaGameSessionTrampolineActivity();
TestActivityPage.assertPageIsLaunched();
}
@Test
public void launch_targetActivityFinishesSuccessfully_futureCompletedWithSameResults() {
AndroidFuture<GameSessionActivityResult> resultFuture =
startTestActivityViaGameSessionTrampolineActivity();
TestActivityPage.assertPageIsLaunched();
TestActivityPage.clickFinish();
GameSessionActivityResult expectedResult =
new GameSessionActivityResult(Activity.RESULT_OK, TestActivity.RESULT_INTENT);
assertEquals(resultFuture, expectedResult);
TestActivityPage.assertPageIsNotLaunched();
}
@Test
public void launch_trampolineActivityProcessDeath_futureCompletedWithSameResults() {
setAlwaysFinishActivities(true);
AndroidFuture<GameSessionActivityResult> resultFuture =
startTestActivityViaGameSessionTrampolineActivity();
TestActivityPage.assertPageIsLaunched();
TestActivityPage.clickFinish();
GameSessionActivityResult expectedResult =
new GameSessionActivityResult(Activity.RESULT_OK, TestActivity.RESULT_INTENT);
assertEquals(resultFuture, expectedResult);
TestActivityPage.assertPageIsNotLaunched();
}
private static void assertEquals(
AndroidFuture<GameSessionActivityResult> actualFuture,
GameSessionActivityResult expected) {
try {
assertEquals(actualFuture.get(20, TimeUnit.SECONDS), expected);
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
private static void assertEquals(
GameSessionActivityResult actual,
GameSessionActivityResult expected) {
assertThat(actual.getResultCode()).isEqualTo(expected.getResultCode());
assertThat(actual.getData()).filtersEquallyTo(actual.getData());
}
private static void setAlwaysFinishActivities(boolean isEnabled) {
try {
ActivityManager.getService().setAlwaysFinish(isEnabled);
} catch (RemoteException ex) {
throw new IllegalStateException(ex);
}
}
private static AndroidFuture<GameSessionActivityResult>
startTestActivityViaGameSessionTrampolineActivity() {
Intent testActivityIntent = new Intent();
testActivityIntent.setClass(getInstrumentation().getTargetContext(), TestActivity.class);
return startGameSessionTrampolineActivity(testActivityIntent);
}
private static AndroidFuture<GameSessionActivityResult> startGameSessionTrampolineActivity(
Intent targetIntent) {
AndroidFuture<GameSessionActivityResult> resultFuture = new AndroidFuture<>();
Intent trampolineActivityIntent = GameSessionTrampolineActivity.createIntent(targetIntent,
null, resultFuture);
trampolineActivityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
getInstrumentation().getTargetContext().startActivity(trampolineActivityIntent);
getInstrumentation().waitForIdleSync();
return resultFuture;
}
private static class TestActivityPage {
private TestActivityPage() {}
public static void assertPageIsLaunched() {
onView(withText(TestActivity.PAGE_TITLE_TEXT)).check(matches(isDisplayed()));
}
public static void assertPageIsNotLaunched() {
try {
onView(withText(TestActivity.PAGE_TITLE_TEXT)).check(doesNotExist());
} catch (NoActivityResumedException ex) {
// Do nothing
}
}
public static void clickFinish() {
onView(allOf(withText(TestActivity.FINISH_BUTTON_TEXT), isClickable())).perform(
click());
getInstrumentation().waitForIdleSync();
}
}
public static class TestActivity extends Activity {
private static final String PAGE_TITLE_TEXT = "GameSessionTestActivity";
private static final String FINISH_BUTTON_TEXT = "Finish Test Activity";
private static final Intent RESULT_INTENT = new Intent("com.test.action.VIEW");
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LinearLayout contentLayout = new LinearLayout(this);
contentLayout.setOrientation(LinearLayout.VERTICAL);
TextView titleTextView = new TextView(this);
titleTextView.setText(PAGE_TITLE_TEXT);
contentLayout.addView(titleTextView);
Button finishActivityButton = new Button(this);
finishActivityButton.setText(FINISH_BUTTON_TEXT);
finishActivityButton.setOnClickListener((unused) -> {
setResult(Activity.RESULT_OK, RESULT_INTENT);
finish();
});
contentLayout.addView(finishActivityButton);
setContentView(contentLayout);
}
}
}