blob: bfb2fd57975f8e15b1e7a18954e8cabc9ba40e4e [file] [log] [blame]
/*
* 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 android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.view.Display.INVALID_DISPLAY;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityThread;
import android.app.ActivityThread.ActivityClientRecord;
import android.app.IApplicationThread;
import android.app.PictureInPictureParams;
import android.app.ResourcesManager;
import android.app.servertransaction.ActivityConfigurationChangeItem;
import android.app.servertransaction.ActivityRelaunchItem;
import android.app.servertransaction.ClientTransaction;
import android.app.servertransaction.ClientTransactionItem;
import android.app.servertransaction.ConfigurationChangeItem;
import android.app.servertransaction.NewIntentItem;
import android.app.servertransaction.ResumeActivityItem;
import android.app.servertransaction.StopActivityItem;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.os.Bundle;
import android.os.IBinder;
import android.platform.test.annotations.Presubmit;
import android.util.DisplayMetrics;
import android.util.MergedConfiguration;
import android.view.Display;
import android.view.View;
import androidx.test.filters.FlakyTest;
import androidx.test.filters.MediumTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.content.ReferrerIntent;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
/**
* Test for verifying {@link android.app.ActivityThread} class.
* Build/Install/Run:
* atest FrameworksCoreTests:android.app.activity.ActivityThreadTest
*/
@RunWith(AndroidJUnit4.class)
@MediumTest
@Presubmit
public class ActivityThreadTest {
private static final int TIMEOUT_SEC = 10;
// 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 */);
private ArrayList<VirtualDisplay> mCreatedVirtualDisplays;
@After
public void tearDown() {
if (mCreatedVirtualDisplays != null) {
mCreatedVirtualDisplays.forEach(VirtualDisplay::release);
mCreatedVirtualDisplays = null;
}
}
@Test
public void testTemporaryDirectory() throws Exception {
assertEquals(System.getProperty("java.io.tmpdir"), System.getenv("TMPDIR"));
}
@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));
final ActivityClientRecord r = getActivityClientRecord(activity);
assertFalse(activityThread.performResumeActivity(r, true /* finalStateRequest */,
"test"));
assertFalse(activityThread.performResumeActivity(r, 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 testRecreateActivity() {
relaunchActivityAndAssertPreserveWindow(Activity::recreate);
}
private void relaunchActivityAndAssertPreserveWindow(Consumer<Activity> relaunch) {
final TestActivity activity = mActivityTestRule.launchActivity(new Intent());
final ActivityThread activityThread = activity.getActivityThread();
final IBinder[] token = new IBinder[1];
final View[] decorView = new View[1];
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
token[0] = activity.getActivityToken();
decorView[0] = activity.getWindow().getDecorView();
relaunch.accept(activity);
});
final View[] newDecorView = new View[1];
final Activity[] newActivity = new Activity[1];
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
newActivity[0] = activityThread.getActivity(token[0]);
newDecorView[0] = newActivity[0].getWindow().getDecorView();
});
assertNotEquals("Activity must be relaunched", activity, newActivity[0]);
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_SkipWhenNewerConfigurationPending() {
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 newerConfig = new Configuration();
newerConfig.orientation = orientation == ORIENTATION_LANDSCAPE
? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE;
newerConfig.seq = seq + 2;
activityThread.updatePendingActivityConfiguration(activity.getActivityToken(),
newerConfig);
final Configuration olderConfig = new Configuration();
olderConfig.orientation = orientation;
olderConfig.seq = seq + 1;
final ActivityClientRecord r = getActivityClientRecord(activity);
activityThread.handleActivityConfigurationChanged(r, olderConfig, INVALID_DISPLAY);
assertEquals(numOfConfig, activity.mNumOfConfigChanges);
assertEquals(olderConfig.orientation, activity.mConfig.orientation);
activityThread.handleActivityConfigurationChanged(r, newerConfig, INVALID_DISPLAY);
assertEquals(numOfConfig + 1, activity.mNumOfConfigChanges);
assertEquals(newerConfig.orientation, activity.mConfig.orientation);
});
}
@Test
public void testHandleActivityConfigurationChanged_EnsureUpdatesProcessedInOrder()
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 = ORIENTATION_PORTRAIT;
activityThread.handleActivityConfigurationChanged(getActivityClientRecord(activity),
config, INVALID_DISPLAY);
});
final IApplicationThread appThread = activityThread.getApplicationThread();
final int numOfConfig = activity.mNumOfConfigChanges;
final Configuration processConfigLandscape = new Configuration();
processConfigLandscape.orientation = ORIENTATION_LANDSCAPE;
processConfigLandscape.windowConfiguration.setBounds(new Rect(0, 0, 100, 60));
processConfigLandscape.seq = BASE_SEQ + 1;
final Configuration activityConfigLandscape = new Configuration();
activityConfigLandscape.orientation = ORIENTATION_LANDSCAPE;
activityConfigLandscape.windowConfiguration.setBounds(new Rect(0, 0, 100, 50));
activityConfigLandscape.seq = BASE_SEQ + 2;
final Configuration processConfigPortrait = new Configuration();
processConfigPortrait.orientation = ORIENTATION_PORTRAIT;
processConfigPortrait.windowConfiguration.setBounds(new Rect(0, 0, 60, 100));
processConfigPortrait.seq = BASE_SEQ + 3;
final Configuration activityConfigPortrait = new Configuration();
activityConfigPortrait.orientation = ORIENTATION_PORTRAIT;
activityConfigPortrait.windowConfiguration.setBounds(new Rect(0, 0, 50, 100));
activityConfigPortrait.seq = BASE_SEQ + 4;
activity.mConfigLatch = new CountDownLatch(1);
activity.mTestLatch = new CountDownLatch(1);
ClientTransaction transaction = newTransaction(activityThread, null);
transaction.addCallback(ConfigurationChangeItem.obtain(processConfigLandscape));
appThread.scheduleTransaction(transaction);
transaction = newTransaction(activityThread, activity.getActivityToken());
transaction.addCallback(ActivityConfigurationChangeItem.obtain(activityConfigLandscape));
transaction.addCallback(ConfigurationChangeItem.obtain(processConfigPortrait));
transaction.addCallback(ActivityConfigurationChangeItem.obtain(activityConfigPortrait));
appThread.scheduleTransaction(transaction);
activity.mTestLatch.await(TIMEOUT_SEC, TimeUnit.SECONDS);
activity.mConfigLatch.countDown();
activity.mConfigLatch = null;
activity.mTestLatch = null;
// Check display metrics, bounds should match the portrait activity bounds.
final Rect bounds = activity.getWindowManager().getCurrentWindowMetrics().getBounds();
assertEquals(activityConfigPortrait.windowConfiguration.getBounds(), bounds);
// Ensure that Activity#onConfigurationChanged() not be called because the changes in
// WindowConfiguration shouldn't be reported, and we only apply the latest Configuration
// update in transaction.
assertEquals(numOfConfig, activity.mNumOfConfigChanges);
}
@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 = ORIENTATION_PORTRAIT;
activityThread.handleActivityConfigurationChanged(getActivityClientRecord(activity),
config, 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.orientation = ORIENTATION_LANDSCAPE;
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(TIMEOUT_SEC, TimeUnit.SECONDS);
config = new Configuration();
config.seq = BASE_SEQ + 2;
config.orientation = ORIENTATION_PORTRAIT;
appThread.scheduleTransaction(newActivityConfigTransaction(activity, config));
config = new Configuration();
config.seq = BASE_SEQ + 3;
config.orientation = ORIENTATION_LANDSCAPE;
appThread.scheduleTransaction(newActivityConfigTransaction(activity, config));
config = new Configuration();
config.seq = BASE_SEQ + 4;
config.orientation = ORIENTATION_PORTRAIT;
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(ORIENTATION_PORTRAIT, activity.mConfig.orientation);
}
@Test
public void testOrientationChanged_DoesntOverrideVirtualDisplayOrientation() {
final TestActivity activity = mActivityTestRule.launchActivity(new Intent());
final ActivityThread activityThread = activity.getActivityThread();
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
Context appContext = activity.getApplication();
Configuration originalAppConfig =
new Configuration(appContext.getResources().getConfiguration());
int virtualDisplayWidth;
int virtualDisplayHeight;
if (originalAppConfig.orientation == ORIENTATION_PORTRAIT) {
virtualDisplayWidth = 100;
virtualDisplayHeight = 200;
} else {
virtualDisplayWidth = 200;
virtualDisplayHeight = 100;
}
final Display virtualDisplay = createVirtualDisplay(appContext,
virtualDisplayWidth, virtualDisplayHeight);
Context virtualDisplayContext = appContext.createDisplayContext(virtualDisplay);
int originalVirtualDisplayOrientation = virtualDisplayContext.getResources()
.getConfiguration().orientation;
// Perform global config change and verify there is no config change in derived display
// context.
Configuration newAppConfig = new Configuration(originalAppConfig);
newAppConfig.seq++;
newAppConfig.orientation = newAppConfig.orientation == ORIENTATION_PORTRAIT
? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT;
activityThread.updatePendingConfiguration(newAppConfig);
activityThread.handleConfigurationChanged(newAppConfig);
try {
assertEquals("Virtual display orientation must not change when process"
+ " configuration orientation changes.",
originalVirtualDisplayOrientation,
virtualDisplayContext.getResources().getConfiguration().orientation);
} finally {
// Make sure to reset the process config to prevent side effects to other
// tests.
Configuration activityThreadConfig = activityThread.getConfiguration();
activityThreadConfig.seq = originalAppConfig.seq - 1;
Configuration resourceManagerConfig = ResourcesManager.getInstance()
.getConfiguration();
resourceManagerConfig.seq = originalAppConfig.seq - 1;
activityThread.updatePendingConfiguration(originalAppConfig);
activityThread.handleConfigurationChanged(originalAppConfig);
}
});
}
@Test
public void testActivityOrientationChanged_DoesntOverrideVirtualDisplayOrientation() {
final TestActivity activity = mActivityTestRule.launchActivity(new Intent());
final ActivityThread activityThread = activity.getActivityThread();
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
Configuration originalActivityConfig =
new Configuration(activity.getResources().getConfiguration());
int virtualDisplayWidth;
int virtualDisplayHeight;
if (originalActivityConfig.orientation == ORIENTATION_PORTRAIT) {
virtualDisplayWidth = 100;
virtualDisplayHeight = 200;
} else {
virtualDisplayWidth = 200;
virtualDisplayHeight = 100;
}
final Display virtualDisplay = createVirtualDisplay(activity,
virtualDisplayWidth, virtualDisplayHeight);
Context virtualDisplayContext = activity.createDisplayContext(virtualDisplay);
int originalVirtualDisplayOrientation = virtualDisplayContext.getResources()
.getConfiguration().orientation;
// Perform activity config change and verify there is no config change in derived
// display context.
Configuration newActivityConfig = new Configuration(originalActivityConfig);
newActivityConfig.seq++;
newActivityConfig.orientation = newActivityConfig.orientation == ORIENTATION_PORTRAIT
? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT;
final ActivityClientRecord r = getActivityClientRecord(activity);
activityThread.updatePendingActivityConfiguration(activity.getActivityToken(),
newActivityConfig);
activityThread.handleActivityConfigurationChanged(r, newActivityConfig,
INVALID_DISPLAY);
assertEquals("Virtual display orientation must not change when activity"
+ " configuration orientation changes.",
originalVirtualDisplayOrientation,
virtualDisplayContext.getResources().getConfiguration().orientation);
});
}
@Test
@FlakyTest(bugId = 176134235)
public void testHandleConfigurationChanged_DoesntOverrideActivityConfig() {
final TestActivity activity = mActivityTestRule.launchActivity(new Intent());
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
final Configuration oldActivityConfig =
new Configuration(activity.getResources().getConfiguration());
final DisplayMetrics oldActivityMetrics = new DisplayMetrics();
activity.getDisplay().getMetrics(oldActivityMetrics);
final Resources oldAppResources = activity.getApplication().getResources();
final Configuration oldAppConfig =
new Configuration(oldAppResources.getConfiguration());
final DisplayMetrics oldApplicationMetrics = new DisplayMetrics();
oldApplicationMetrics.setTo(oldAppResources.getDisplayMetrics());
assertEquals("Process config must match the top activity config by default",
0, oldActivityConfig.diffPublicOnly(oldAppConfig));
assertEquals("Process config must match the top activity config by default",
oldActivityMetrics, oldApplicationMetrics);
// Update the application configuration separately from activity config
final Configuration newAppConfig = new Configuration(oldAppConfig);
newAppConfig.densityDpi += 100;
newAppConfig.screenHeightDp += 100;
final Rect newBounds = new Rect(newAppConfig.windowConfiguration.getAppBounds());
newBounds.bottom += 100;
newAppConfig.windowConfiguration.setAppBounds(newBounds);
newAppConfig.windowConfiguration.setBounds(newBounds);
newAppConfig.seq++;
final ActivityThread activityThread = activity.getActivityThread();
activityThread.handleConfigurationChanged(newAppConfig);
// Verify that application config update was applied, but didn't change activity config.
assertEquals("Activity config must not change if the process config changes",
oldActivityConfig, activity.getResources().getConfiguration());
final DisplayMetrics newActivityMetrics = new DisplayMetrics();
activity.getDisplay().getMetrics(newActivityMetrics);
assertEquals("Activity display size must not change if the process config changes",
oldActivityMetrics, newActivityMetrics);
final Resources newAppResources = activity.getApplication().getResources();
assertEquals("Application config must be updated",
newAppConfig, newAppResources.getConfiguration());
final DisplayMetrics newApplicationMetrics = new DisplayMetrics();
newApplicationMetrics.setTo(newAppResources.getDisplayMetrics());
assertNotEquals("Application display size must be updated after config update",
oldApplicationMetrics, newApplicationMetrics);
assertNotEquals("Application display size must be updated after config update",
newActivityMetrics, newApplicationMetrics);
});
}
@Test
public void testHandleProcessConfigurationChanged_DependOnProcessState() {
final ActivityThread activityThread = ActivityThread.currentActivityThread();
final Configuration origConfig = activityThread.getConfiguration();
final int newDpi = origConfig.densityDpi + 10;
final Configuration newConfig = new Configuration(origConfig);
newConfig.seq++;
newConfig.densityDpi = newDpi;
activityThread.updateProcessState(ActivityManager.PROCESS_STATE_CACHED_ACTIVITY,
false /* fromIPC */);
applyProcessConfiguration(activityThread, newConfig);
try {
// In the cached state, the configuration is only set as pending and not applied.
assertEquals(origConfig.densityDpi, activityThread.getConfiguration().densityDpi);
assertTrue(activityThread.isCachedProcessState());
} finally {
// The foreground state is the default state of instrumentation.
activityThread.updateProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE,
false /* fromIPC */);
}
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
try {
// The state becomes non-cached, the pending configuration should be applied.
assertEquals(newConfig.densityDpi, activityThread.getConfiguration().densityDpi);
assertFalse(activityThread.isCachedProcessState());
} finally {
// Restore to the original configuration.
activityThread.getConfiguration().seq = origConfig.seq - 1;
applyProcessConfiguration(activityThread, origConfig);
}
}
private static void applyProcessConfiguration(ActivityThread thread, Configuration config) {
final ClientTransaction clientTransaction = newTransaction(thread,
null /* activityToken */);
clientTransaction.addCallback(ConfigurationChangeItem.obtain(config));
final IApplicationThread appThread = thread.getApplicationThread();
try {
appThread.scheduleTransaction(clientTransaction);
} catch (Exception ignored) {
}
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
@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, true));
});
assertThat(activity.isResumed()).isTrue();
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
activityThread.executeTransaction(newStopTransaction(activity));
activityThread.executeTransaction(newNewIntentTransaction(activity, rIntents, false));
});
assertThat(activity.isResumed()).isFalse();
}
@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();
final ActivityClientRecord r = getActivityClientRecord(activity);
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
activityThread.handlePictureInPictureRequested(r);
});
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();
final ActivityClientRecord r = getActivityClientRecord(activity);
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
activityThread.handlePictureInPictureRequested(r);
});
assertTrue(activity.pipRequested());
assertTrue(activity.enterPipSkipped());
}
@Test
public void testHandlePictureInPictureRequested_notOverridden() {
final TestActivity activity = mActivityTestRule.launchActivity(new Intent());
final ActivityThread activityThread = activity.getActivityThread();
final ActivityClientRecord r = getActivityClientRecord(activity);
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
activityThread.handlePictureInPictureRequested(r);
});
assertTrue(activity.pipRequested());
assertFalse(activity.enteredPip());
assertFalse(activity.enterPipSkipped());
}
/**
* Calls {@link ActivityThread#handleActivityConfigurationChanged(ActivityClientRecord,
* 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 ActivityClientRecord r = getActivityClientRecord(activity);
final int numOfConfig = activity.mNumOfConfigChanges;
Configuration config = new Configuration();
config.orientation = ORIENTATION_PORTRAIT;
config.seq = seq;
activityThread.handleActivityConfigurationChanged(r, config, INVALID_DISPLAY);
if (activity.mNumOfConfigChanges > numOfConfig) {
return config.seq;
}
config = new Configuration();
config.orientation = ORIENTATION_LANDSCAPE;
config.seq = seq + 1;
activityThread.handleActivityConfigurationChanged(r, config, INVALID_DISPLAY);
return config.seq;
}
private Display createVirtualDisplay(Context context, int w, int h) {
final DisplayManager dm = context.getSystemService(DisplayManager.class);
final VirtualDisplay virtualDisplay = dm.createVirtualDisplay("virtual-display", w, h,
200 /* densityDpi */, null /* surface */, 0 /* flags */);
if (mCreatedVirtualDisplays == null) {
mCreatedVirtualDisplays = new ArrayList<>();
}
mCreatedVirtualDisplays.add(virtualDisplay);
return virtualDisplay.getDisplay();
}
private static ActivityClientRecord getActivityClientRecord(Activity activity) {
final ActivityThread thread = activity.getActivityThread();
final IBinder token = activity.getActivityToken();
return thread.getActivityClient(token);
}
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) {
return newTransaction(activity.getActivityThread(), activity.getActivityToken());
}
private static ClientTransaction newTransaction(ActivityThread activityThread,
@Nullable IBinder activityToken) {
return ClientTransaction.obtain(activityThread.getApplicationThread(), activityToken);
}
// 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
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().getDecorView().setKeepScreenOn(true);
setShowWhenLocked(true);
setTurnScreenOn(true);
}
@Override
public void onConfigurationChanged(Configuration config) {
super.onConfigurationChanged(config);
mConfig.setTo(config);
++mNumOfConfigChanges;
if (mConfigLatch != null) {
if (mTestLatch != null) {
mTestLatch.countDown();
}
try {
mConfigLatch.await(TIMEOUT_SEC, TimeUnit.SECONDS);
} 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;
}
}
}