diff options
| author | 2022-04-27 22:22:59 +0000 | |
|---|---|---|
| committer | 2022-04-27 22:22:59 +0000 | |
| commit | eeebac455b0501c4efbcec388539c5ca4f3048a1 (patch) | |
| tree | dd076330e8abe037b7788f7f53031f153d68480c | |
| parent | 30fdd9646cecbd47af35c5aa1fbc2672f03d0ff6 (diff) | |
| parent | 37db6025c6998d47d09e79bf04547722a5028705 (diff) | |
Merge "Ensure GameSessionTrampolineActivity launches intent once." into tm-dev
6 files changed, 282 insertions, 13 deletions
diff --git a/core/java/android/service/games/GameSession.java b/core/java/android/service/games/GameSession.java index 01152943efe3..e8d53d351795 100644 --- a/core/java/android/service/games/GameSession.java +++ b/core/java/android/service/games/GameSession.java @@ -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( diff --git a/core/java/android/service/games/GameSessionActivityResult.java b/core/java/android/service/games/GameSessionActivityResult.java index a2ec6ada010c..c8099e6e5eff 100644 --- a/core/java/android/service/games/GameSessionActivityResult.java +++ b/core/java/android/service/games/GameSessionActivityResult.java @@ -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; } diff --git a/core/java/android/service/games/GameSessionTrampolineActivity.java b/core/java/android/service/games/GameSessionTrampolineActivity.java index 3d97d0f59b33..e890876df3e7 100644 --- a/core/java/android/service/games/GameSessionTrampolineActivity.java +++ b/core/java/android/service/games/GameSessionTrampolineActivity.java @@ -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), @@ -64,6 +109,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) { // Something went very wrong if we hit this code path, and we should bail. diff --git a/services/tests/mockingservicestests/Android.bp b/services/tests/mockingservicestests/Android.bp index 670c1596e15e..08c68b9a469e 100644 --- a/services/tests/mockingservicestests/Android.bp +++ b/services/tests/mockingservicestests/Android.bp @@ -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", diff --git a/services/tests/mockingservicestests/AndroidManifest.xml b/services/tests/mockingservicestests/AndroidManifest.xml index 7714cf0ca094..07b763dcd85b 100644 --- a/services/tests/mockingservicestests/AndroidManifest.xml +++ b/services/tests/mockingservicestests/AndroidManifest.xml @@ -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 diff --git a/services/tests/mockingservicestests/src/android/service/games/GameSessionTrampolineActivityTest.java b/services/tests/mockingservicestests/src/android/service/games/GameSessionTrampolineActivityTest.java new file mode 100644 index 000000000000..d68b517ca8cd --- /dev/null +++ b/services/tests/mockingservicestests/src/android/service/games/GameSessionTrampolineActivityTest.java @@ -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); + } + } +} |