| /* |
| * Copyright 2018 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.app.activity; |
| |
| import static android.content.Intent.ACTION_EDIT; |
| import static android.content.Intent.ACTION_VIEW; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertNotEquals; |
| import static org.junit.Assert.assertNull; |
| import static org.junit.Assert.assertTrue; |
| import static org.testng.Assert.assertFalse; |
| |
| import android.app.Activity; |
| import android.app.ActivityThread; |
| import android.app.IApplicationThread; |
| import android.app.PictureInPictureParams; |
| import android.app.servertransaction.ActivityConfigurationChangeItem; |
| import android.app.servertransaction.ActivityRelaunchItem; |
| import android.app.servertransaction.ClientTransaction; |
| import android.app.servertransaction.ClientTransactionItem; |
| import android.app.servertransaction.NewIntentItem; |
| import android.app.servertransaction.ResumeActivityItem; |
| import android.app.servertransaction.StopActivityItem; |
| import android.content.Intent; |
| import android.content.res.Configuration; |
| import android.os.IBinder; |
| import android.util.MergedConfiguration; |
| import android.view.Display; |
| import android.view.View; |
| |
| import androidx.test.InstrumentationRegistry; |
| import androidx.test.filters.MediumTest; |
| import androidx.test.rule.ActivityTestRule; |
| import androidx.test.runner.AndroidJUnit4; |
| |
| import com.android.internal.content.ReferrerIntent; |
| |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.concurrent.CountDownLatch; |
| |
| /** |
| * Test for verifying {@link android.app.ActivityThread} class. |
| * Build/Install/Run: |
| * atest FrameworksCoreTests:android.app.activity.ActivityThreadTest |
| */ |
| @RunWith(AndroidJUnit4.class) |
| @MediumTest |
| public class ActivityThreadTest { |
| |
| // The first sequence number to try with. Use a large number to avoid conflicts with the first a |
| // few sequence numbers the framework used to launch the test activity. |
| private static final int BASE_SEQ = 10000; |
| |
| private final ActivityTestRule<TestActivity> mActivityTestRule = |
| new ActivityTestRule<>(TestActivity.class, true /* initialTouchMode */, |
| false /* launchActivity */); |
| |
| @Test |
| public void testDoubleRelaunch() throws Exception { |
| final Activity activity = mActivityTestRule.launchActivity(new Intent()); |
| final IApplicationThread appThread = activity.getActivityThread().getApplicationThread(); |
| |
| appThread.scheduleTransaction(newRelaunchResumeTransaction(activity)); |
| appThread.scheduleTransaction(newRelaunchResumeTransaction(activity)); |
| InstrumentationRegistry.getInstrumentation().waitForIdleSync(); |
| } |
| |
| @Test |
| public void testResumeAfterRelaunch() throws Exception { |
| final Activity activity = mActivityTestRule.launchActivity(new Intent()); |
| final IApplicationThread appThread = activity.getActivityThread().getApplicationThread(); |
| |
| appThread.scheduleTransaction(newRelaunchResumeTransaction(activity)); |
| appThread.scheduleTransaction(newResumeTransaction(activity)); |
| InstrumentationRegistry.getInstrumentation().waitForIdleSync(); |
| } |
| |
| /** Verify that repeated resume requests to activity will be ignored. */ |
| @Test |
| public void testRepeatedResume() throws Exception { |
| final Activity activity = mActivityTestRule.launchActivity(new Intent()); |
| final ActivityThread activityThread = activity.getActivityThread(); |
| InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { |
| activityThread.executeTransaction(newResumeTransaction(activity)); |
| assertNull(activityThread.performResumeActivity(activity.getActivityToken(), |
| true /* finalStateRequest */, "test")); |
| |
| assertNull(activityThread.performResumeActivity(activity.getActivityToken(), |
| false /* finalStateRequest */, "test")); |
| }); |
| } |
| |
| /** Verify that custom intent set via Activity#setIntent() is preserved on relaunch. */ |
| @Test |
| public void testCustomIntentPreservedOnRelaunch() throws Exception { |
| final Intent initIntent = new Intent(); |
| initIntent.setAction(ACTION_VIEW); |
| final Activity activity = mActivityTestRule.launchActivity(initIntent); |
| IBinder token = activity.getActivityToken(); |
| |
| final ActivityThread activityThread = activity.getActivityThread(); |
| InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { |
| // Recreate and check that intent is still the same. |
| activity.recreate(); |
| |
| final Activity newActivity = activityThread.getActivity(token); |
| assertTrue("Original intent must be preserved after recreate", |
| initIntent.filterEquals(newActivity.getIntent())); |
| |
| // Set custom intent, recreate and check if it is preserved. |
| final Intent customIntent = new Intent(); |
| customIntent.setAction(ACTION_EDIT); |
| newActivity.setIntent(customIntent); |
| |
| activity.recreate(); |
| |
| final Activity lastActivity = activityThread.getActivity(token); |
| assertTrue("Custom intent must be preserved after recreate", |
| customIntent.filterEquals(lastActivity.getIntent())); |
| }); |
| } |
| |
| @Test |
| public void testHandleActivityConfigurationChanged() { |
| final TestActivity activity = mActivityTestRule.launchActivity(new Intent()); |
| |
| InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { |
| final int numOfConfig = activity.mNumOfConfigChanges; |
| applyConfigurationChange(activity, BASE_SEQ); |
| assertEquals(numOfConfig + 1, activity.mNumOfConfigChanges); |
| }); |
| } |
| |
| @Test |
| public void testHandleActivity_assetsChanged() { |
| final TestActivity activity = mActivityTestRule.launchActivity(new Intent()); |
| |
| final IBinder[] token = new IBinder[1]; |
| final View[] decorView = new View[1]; |
| |
| InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { |
| final ActivityThread activityThread = activity.getActivityThread(); |
| |
| token[0] = activity.getActivityToken(); |
| decorView[0] = activity.getWindow().getDecorView(); |
| |
| // Relaunches all activities |
| activityThread.handleApplicationInfoChanged(activity.getApplicationInfo()); |
| }); |
| |
| final View[] newDecorView = new View[1]; |
| InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { |
| final ActivityThread activityThread = activity.getActivityThread(); |
| |
| final Activity newActivity = activityThread.getActivity(token[0]); |
| newDecorView[0] = activity.getWindow().getDecorView(); |
| }); |
| |
| assertEquals("Window must be preserved", decorView[0], newDecorView[0]); |
| } |
| |
| @Test |
| public void testHandleActivityConfigurationChanged_DropStaleConfigurations() { |
| final TestActivity activity = mActivityTestRule.launchActivity(new Intent()); |
| |
| InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { |
| // Set the sequence number to BASE_SEQ. |
| applyConfigurationChange(activity, BASE_SEQ); |
| |
| final int orientation = activity.mConfig.orientation; |
| final int numOfConfig = activity.mNumOfConfigChanges; |
| |
| // Try to apply an old configuration change. |
| applyConfigurationChange(activity, BASE_SEQ - 1); |
| assertEquals(numOfConfig, activity.mNumOfConfigChanges); |
| assertEquals(orientation, activity.mConfig.orientation); |
| }); |
| } |
| |
| @Test |
| public void testHandleActivityConfigurationChanged_ApplyNewConfigurations() { |
| final TestActivity activity = mActivityTestRule.launchActivity(new Intent()); |
| |
| InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { |
| // Set the sequence number to BASE_SEQ and record the final sequence number it used. |
| final int seq = applyConfigurationChange(activity, BASE_SEQ); |
| |
| final int orientation = activity.mConfig.orientation; |
| final int numOfConfig = activity.mNumOfConfigChanges; |
| |
| // Try to apply an new configuration change. |
| applyConfigurationChange(activity, seq + 1); |
| assertEquals(numOfConfig + 1, activity.mNumOfConfigChanges); |
| assertNotEquals(orientation, activity.mConfig.orientation); |
| }); |
| } |
| |
| @Test |
| public void testHandleActivityConfigurationChanged_PickNewerPendingConfiguration() { |
| final TestActivity activity = mActivityTestRule.launchActivity(new Intent()); |
| |
| InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { |
| // Set the sequence number to BASE_SEQ and record the final sequence number it used. |
| final int seq = applyConfigurationChange(activity, BASE_SEQ); |
| |
| final int orientation = activity.mConfig.orientation; |
| final int numOfConfig = activity.mNumOfConfigChanges; |
| |
| final ActivityThread activityThread = activity.getActivityThread(); |
| |
| final Configuration pendingConfig = new Configuration(); |
| pendingConfig.orientation = orientation == Configuration.ORIENTATION_LANDSCAPE |
| ? Configuration.ORIENTATION_PORTRAIT |
| : Configuration.ORIENTATION_LANDSCAPE; |
| pendingConfig.seq = seq + 2; |
| activityThread.updatePendingActivityConfiguration(activity.getActivityToken(), |
| pendingConfig); |
| |
| final Configuration newConfig = new Configuration(); |
| newConfig.orientation = orientation; |
| newConfig.seq = seq + 1; |
| |
| activityThread.handleActivityConfigurationChanged(activity.getActivityToken(), |
| newConfig, Display.INVALID_DISPLAY); |
| assertEquals(numOfConfig + 1, activity.mNumOfConfigChanges); |
| assertEquals(pendingConfig.orientation, activity.mConfig.orientation); |
| }); |
| } |
| |
| @Test |
| public void testHandleActivityConfigurationChanged_OnlyAppliesNewestConfiguration() |
| throws Exception { |
| final TestActivity activity = mActivityTestRule.launchActivity(new Intent()); |
| |
| final ActivityThread activityThread = activity.getActivityThread(); |
| InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { |
| final Configuration config = new Configuration(); |
| config.seq = BASE_SEQ; |
| config.orientation = Configuration.ORIENTATION_PORTRAIT; |
| |
| activityThread.handleActivityConfigurationChanged(activity.getActivityToken(), |
| config, Display.INVALID_DISPLAY); |
| }); |
| |
| final int numOfConfig = activity.mNumOfConfigChanges; |
| final IApplicationThread appThread = activityThread.getApplicationThread(); |
| |
| activity.mConfigLatch = new CountDownLatch(1); |
| activity.mTestLatch = new CountDownLatch(1); |
| |
| Configuration config = new Configuration(); |
| config.seq = BASE_SEQ + 1; |
| config.smallestScreenWidthDp = 100; |
| appThread.scheduleTransaction(newActivityConfigTransaction(activity, config)); |
| |
| // Wait until the main thread is performing the configuration change for the configuration |
| // with sequence number BASE_SEQ + 1 before proceeding. This is to mimic the situation where |
| // the activity takes very long time to process configuration changes. |
| activity.mTestLatch.await(); |
| |
| config = new Configuration(); |
| config.seq = BASE_SEQ + 2; |
| config.smallestScreenWidthDp = 200; |
| appThread.scheduleTransaction(newActivityConfigTransaction(activity, config)); |
| |
| config = new Configuration(); |
| config.seq = BASE_SEQ + 3; |
| config.smallestScreenWidthDp = 300; |
| appThread.scheduleTransaction(newActivityConfigTransaction(activity, config)); |
| |
| config = new Configuration(); |
| config.seq = BASE_SEQ + 4; |
| config.smallestScreenWidthDp = 400; |
| appThread.scheduleTransaction(newActivityConfigTransaction(activity, config)); |
| |
| activity.mConfigLatch.countDown(); |
| InstrumentationRegistry.getInstrumentation().waitForIdleSync(); |
| |
| activity.mConfigLatch = null; |
| activity.mTestLatch = null; |
| |
| // Only two more configuration changes: one with seq BASE_SEQ + 1; another with seq |
| // BASE_SEQ + 4. Configurations scheduled in between should be dropped. |
| assertEquals(numOfConfig + 2, activity.mNumOfConfigChanges); |
| assertEquals(400, activity.mConfig.smallestScreenWidthDp); |
| } |
| |
| @Test |
| public void testResumeAfterNewIntent() { |
| final Activity activity = mActivityTestRule.launchActivity(new Intent()); |
| final ActivityThread activityThread = activity.getActivityThread(); |
| final ArrayList<ReferrerIntent> rIntents = new ArrayList<>(); |
| rIntents.add(new ReferrerIntent(new Intent(), "android.app.activity")); |
| |
| InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { |
| activityThread.executeTransaction(newNewIntentTransaction(activity, rIntents, false)); |
| }); |
| assertThat(activity.isResumed()).isFalse(); |
| |
| InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { |
| activityThread.executeTransaction(newNewIntentTransaction(activity, rIntents, true)); |
| }); |
| assertThat(activity.isResumed()).isTrue(); |
| } |
| |
| @Test |
| public void testHandlePictureInPictureRequested_overriddenToEnter() { |
| final Intent startIntent = new Intent(); |
| startIntent.putExtra(TestActivity.PIP_REQUESTED_OVERRIDE_ENTER, true); |
| final TestActivity activity = mActivityTestRule.launchActivity(startIntent); |
| final ActivityThread activityThread = activity.getActivityThread(); |
| |
| InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { |
| activityThread.handlePictureInPictureRequested(activity.getActivityToken()); |
| }); |
| |
| assertTrue(activity.pipRequested()); |
| assertTrue(activity.enteredPip()); |
| } |
| |
| @Test |
| public void testHandlePictureInPictureRequested_overriddenToSkip() { |
| final Intent startIntent = new Intent(); |
| startIntent.putExtra(TestActivity.PIP_REQUESTED_OVERRIDE_SKIP, true); |
| final TestActivity activity = mActivityTestRule.launchActivity(startIntent); |
| final ActivityThread activityThread = activity.getActivityThread(); |
| |
| InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { |
| activityThread.handlePictureInPictureRequested(activity.getActivityToken()); |
| }); |
| |
| assertTrue(activity.pipRequested()); |
| assertTrue(activity.enterPipSkipped()); |
| } |
| |
| @Test |
| public void testHandlePictureInPictureRequested_notOverridden() { |
| final TestActivity activity = mActivityTestRule.launchActivity(new Intent()); |
| final ActivityThread activityThread = activity.getActivityThread(); |
| |
| InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { |
| activityThread.handlePictureInPictureRequested(activity.getActivityToken()); |
| }); |
| |
| assertTrue(activity.pipRequested()); |
| assertFalse(activity.enteredPip()); |
| assertFalse(activity.enterPipSkipped()); |
| } |
| |
| /** |
| * Calls {@link ActivityThread#handleActivityConfigurationChanged(IBinder, Configuration, int)} |
| * to try to push activity configuration to the activity for the given sequence number. |
| * <p> |
| * It uses orientation to push the configuration and it tries a different orientation if the |
| * first attempt doesn't make through, to rule out the possibility that the previous |
| * configuration already has the same orientation. |
| * |
| * @param activity the test target activity |
| * @param seq the specified sequence number |
| * @return the sequence number this method tried with the last time, so that the caller can use |
| * the next sequence number for next configuration update. |
| */ |
| private int applyConfigurationChange(TestActivity activity, int seq) { |
| final ActivityThread activityThread = activity.getActivityThread(); |
| |
| final int numOfConfig = activity.mNumOfConfigChanges; |
| Configuration config = new Configuration(); |
| config.orientation = Configuration.ORIENTATION_PORTRAIT; |
| config.seq = seq; |
| activityThread.handleActivityConfigurationChanged(activity.getActivityToken(), config, |
| Display.INVALID_DISPLAY); |
| |
| if (activity.mNumOfConfigChanges > numOfConfig) { |
| return config.seq; |
| } |
| |
| config = new Configuration(); |
| config.orientation = Configuration.ORIENTATION_LANDSCAPE; |
| config.seq = seq + 1; |
| activityThread.handleActivityConfigurationChanged(activity.getActivityToken(), config, |
| Display.INVALID_DISPLAY); |
| |
| return config.seq; |
| } |
| |
| private static ClientTransaction newRelaunchResumeTransaction(Activity activity) { |
| final ClientTransactionItem callbackItem = ActivityRelaunchItem.obtain(null, |
| null, 0, new MergedConfiguration(), false /* preserveWindow */); |
| final ResumeActivityItem resumeStateRequest = |
| ResumeActivityItem.obtain(true /* isForward */); |
| |
| final ClientTransaction transaction = newTransaction(activity); |
| transaction.addCallback(callbackItem); |
| transaction.setLifecycleStateRequest(resumeStateRequest); |
| |
| return transaction; |
| } |
| |
| private static ClientTransaction newResumeTransaction(Activity activity) { |
| final ResumeActivityItem resumeStateRequest = |
| ResumeActivityItem.obtain(true /* isForward */); |
| |
| final ClientTransaction transaction = newTransaction(activity); |
| transaction.setLifecycleStateRequest(resumeStateRequest); |
| |
| return transaction; |
| } |
| |
| private static ClientTransaction newStopTransaction(Activity activity) { |
| final StopActivityItem stopStateRequest = StopActivityItem.obtain(0 /* configChanges */); |
| |
| final ClientTransaction transaction = newTransaction(activity); |
| transaction.setLifecycleStateRequest(stopStateRequest); |
| |
| return transaction; |
| } |
| |
| private static ClientTransaction newActivityConfigTransaction(Activity activity, |
| Configuration config) { |
| final ActivityConfigurationChangeItem item = ActivityConfigurationChangeItem.obtain(config); |
| |
| final ClientTransaction transaction = newTransaction(activity); |
| transaction.addCallback(item); |
| |
| return transaction; |
| } |
| |
| private static ClientTransaction newNewIntentTransaction(Activity activity, |
| List<ReferrerIntent> intents, boolean resume) { |
| final NewIntentItem item = NewIntentItem.obtain(intents, resume); |
| |
| final ClientTransaction transaction = newTransaction(activity); |
| transaction.addCallback(item); |
| |
| return transaction; |
| } |
| |
| private static ClientTransaction newTransaction(Activity activity) { |
| final IApplicationThread appThread = activity.getActivityThread().getApplicationThread(); |
| return ClientTransaction.obtain(appThread, activity.getActivityToken()); |
| } |
| |
| // Test activity |
| public static class TestActivity extends Activity { |
| static final String PIP_REQUESTED_OVERRIDE_ENTER = "pip_requested_override_enter"; |
| static final String PIP_REQUESTED_OVERRIDE_SKIP = "pip_requested_override_skip"; |
| |
| int mNumOfConfigChanges; |
| final Configuration mConfig = new Configuration(); |
| |
| private boolean mPipRequested; |
| private boolean mPipEntered; |
| private boolean mPipEnterSkipped; |
| |
| /** |
| * A latch used to notify tests that we're about to wait for configuration latch. This |
| * is used to notify test code that preExecute phase for activity configuration change |
| * transaction has passed. |
| */ |
| volatile CountDownLatch mTestLatch; |
| /** |
| * If not {@code null} {@link #onConfigurationChanged(Configuration)} won't return until the |
| * latch reaches 0. |
| */ |
| volatile CountDownLatch mConfigLatch; |
| |
| @Override |
| public void onConfigurationChanged(Configuration config) { |
| super.onConfigurationChanged(config); |
| mConfig.setTo(config); |
| ++mNumOfConfigChanges; |
| |
| if (mConfigLatch != null) { |
| if (mTestLatch != null) { |
| mTestLatch.countDown(); |
| } |
| try { |
| mConfigLatch.await(); |
| } catch (InterruptedException e) { |
| throw new IllegalStateException(e); |
| } |
| } |
| } |
| |
| @Override |
| public boolean onPictureInPictureRequested() { |
| mPipRequested = true; |
| if (getIntent().getBooleanExtra(PIP_REQUESTED_OVERRIDE_ENTER, false)) { |
| enterPictureInPictureMode(new PictureInPictureParams.Builder().build()); |
| mPipEntered = true; |
| return true; |
| } else if (getIntent().getBooleanExtra(PIP_REQUESTED_OVERRIDE_SKIP, false)) { |
| mPipEnterSkipped = true; |
| return false; |
| } |
| return super.onPictureInPictureRequested(); |
| } |
| |
| boolean pipRequested() { |
| return mPipRequested; |
| } |
| |
| boolean enteredPip() { |
| return mPipEntered; |
| } |
| |
| boolean enterPipSkipped() { |
| return mPipEnterSkipped; |
| } |
| } |
| } |