Merge "Revert "Set Navigation to depend on exactly Fragment 1.1.0-alpha05"" into androidx-master-dev
diff --git a/appcompat/resources/build.gradle b/appcompat/resources/build.gradle
index ebdf4d3..b78317e 100644
--- a/appcompat/resources/build.gradle
+++ b/appcompat/resources/build.gradle
@@ -25,11 +25,10 @@
dependencies {
api(project(":annotation"))
-
api("androidx.core:core:1.0.1")
implementation("androidx.collection:collection:1.0.0")
- api("androidx.vectordrawable:vectordrawable:1.0.1")
- api("androidx.vectordrawable:vectordrawable-animated:1.0.0")
+ api(project(":vectordrawable"))
+ api(project(":vectordrawable-animated"))
androidTestImplementation(TEST_EXT_JUNIT)
androidTestImplementation(TEST_CORE)
diff --git a/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatSpinnerTest.java b/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatSpinnerTest.java
index d4267b0..f9695e0 100644
--- a/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatSpinnerTest.java
+++ b/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatSpinnerTest.java
@@ -19,8 +19,10 @@
import static androidx.appcompat.testutils.TestUtilsMatchers.isCombinedBackground;
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.RootMatchers.isPlatformPopup;
+import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
@@ -32,6 +34,7 @@
import android.content.pm.ActivityInfo;
import android.content.res.Resources;
import android.os.SystemClock;
+import android.view.View;
import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes;
@@ -40,6 +43,11 @@
import androidx.appcompat.test.R;
import androidx.core.content.ContextCompat;
import androidx.core.content.res.ResourcesCompat;
+import androidx.test.espresso.ViewAction;
+import androidx.test.espresso.action.CoordinatesProvider;
+import androidx.test.espresso.action.GeneralSwipeAction;
+import androidx.test.espresso.action.Press;
+import androidx.test.espresso.action.Swipe;
import androidx.test.filters.LargeTest;
import androidx.test.filters.MediumTest;
@@ -186,4 +194,74 @@
mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
onView(withText(EARTH)).check(matches(isDisplayed()));
}
+
+ @LargeTest
+ @Test
+ public void testSlowScroll() {
+ onView(withId(R.id.spinner_dropdown_popup_with_scroll)).perform(click());
+
+ final AppCompatSpinner spinner = mContainer
+ .findViewById(R.id.spinner_dropdown_popup_with_scroll);
+ String secondItem = (String) spinner.getAdapter().getItem(1);
+
+ onView(isAssignableFrom(DropDownListView.class)).perform(slowScrollPopup());
+
+ // when we scroll slowly a second time the popup list might jump back to the first element
+ onView(isAssignableFrom(DropDownListView.class)).perform(slowScrollPopup());
+
+ // because we scroll twice with one element height each,
+ // the second item should not be visible
+ onView(withText(secondItem))
+ .check(doesNotExist());
+ }
+
+ private ViewAction slowScrollPopup() {
+ return new GeneralSwipeAction(Swipe.SLOW,
+ new CoordinatesProvider() {
+ @Override
+ public float[] calculateCoordinates(View view) {
+ final float[] middleLocation = getViewMiddleLocation(view);
+ return new float[] {
+ middleLocation[0],
+ middleLocation[1]
+ };
+ }
+ },
+ new CoordinatesProvider() {
+ @Override
+ public float[] calculateCoordinates(View view) {
+ final float[] middleLocation = getViewMiddleLocation(view);
+ return new float[] {
+ middleLocation[0],
+ middleLocation[1] - getElementSize(view)
+ };
+ }
+ },
+ Press.PINPOINT
+ );
+ }
+
+ private float[] getViewMiddleLocation(View view) {
+ final DropDownListView list = (DropDownListView) view;
+
+ final int[] location = new int[2];
+ list.getLocationOnScreen(location);
+
+ final float x = location[0] + list.getWidth() / 2f;
+ final float y = location[1] + list.getHeight() / 2f;
+
+ return new float[] {x, y};
+ }
+
+ private int getElementSize(View view) {
+ final DropDownListView list = (DropDownListView) view;
+
+ final View child = list.getChildAt(0);
+ final int[] location = new int[2];
+ child.getLocationOnScreen(location);
+
+ // espresso doesn't actually scroll for the full amount specified
+ // so we add a little bit more to be safe
+ return child.getHeight() * 2;
+ }
}
diff --git a/appcompat/src/androidTest/java/androidx/appcompat/widget/ToolbarTest.java b/appcompat/src/androidTest/java/androidx/appcompat/widget/ToolbarTest.java
index 047a560..eb6ee0e 100644
--- a/appcompat/src/androidTest/java/androidx/appcompat/widget/ToolbarTest.java
+++ b/appcompat/src/androidTest/java/androidx/appcompat/widget/ToolbarTest.java
@@ -32,7 +32,6 @@
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.test.R;
import androidx.appcompat.testutils.TestUtils;
-import androidx.test.espresso.Espresso;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -155,18 +154,9 @@
}
@Test
- public void testToolbarOverflowIconWithThemedCSL() throws Throwable {
+ public void testToolbarOverflowIconWithThemedCSL() {
final Toolbar toolbar = mActivity.findViewById(R.id.toolbar_themedcsl_colorcontrolnormal);
- // Inflate a menu so that the overflow is displayed
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- toolbar.inflateMenu(R.menu.popup_menu);
- }
- });
- Espresso.onIdle();
-
// Assert that the overflow icon is tinted magenta, as per the theme
final Drawable icon = toolbar.getOverflowIcon();
assertNotNull(icon);
diff --git a/appcompat/src/androidTest/res/layout/appcompat_spinner_activity.xml b/appcompat/src/androidTest/res/layout/appcompat_spinner_activity.xml
index da55bdf..38228a9 100644
--- a/appcompat/src/androidTest/res/layout/appcompat_spinner_activity.xml
+++ b/appcompat/src/androidTest/res/layout/appcompat_spinner_activity.xml
@@ -93,6 +93,13 @@
android:layout_height="wrap_content"
android:entries="@array/planets_array"
android:spinnerMode="dropdown" />
+
+ <androidx.appcompat.widget.AppCompatSpinner
+ android:id="@+id/spinner_dropdown_popup_with_scroll"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:entries="@array/numbers_array"
+ android:spinnerMode="dropdown" />
</LinearLayout>
</ScrollView>
diff --git a/appcompat/src/androidTest/res/layout/appcompat_toolbar_activity.xml b/appcompat/src/androidTest/res/layout/appcompat_toolbar_activity.xml
index c1bab6a..0d11cab 100644
--- a/appcompat/src/androidTest/res/layout/appcompat_toolbar_activity.xml
+++ b/appcompat/src/androidTest/res/layout/appcompat_toolbar_activity.xml
@@ -56,6 +56,7 @@
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="@style/ThemeOverlay.ThemedCslMagenta"
+ app:menu="@menu/popup_menu"
app:subtitle="Subtitle"
app:title="Title" />
diff --git a/appcompat/src/androidTest/res/values/strings.xml b/appcompat/src/androidTest/res/values/strings.xml
index 964c32b..0188024 100644
--- a/appcompat/src/androidTest/res/values/strings.xml
+++ b/appcompat/src/androidTest/res/values/strings.xml
@@ -78,6 +78,48 @@
<item>Neptune</item>
<item>Pluto</item>
</string-array>
+ <string-array name="numbers_array">
+ <item>0</item>
+ <item>1</item>
+ <item>2</item>
+ <item>3</item>
+ <item>4</item>
+ <item>5</item>
+ <item>6</item>
+ <item>7</item>
+ <item>8</item>
+ <item>9</item>
+ <item>10</item>
+ <item>11</item>
+ <item>12</item>
+ <item>13</item>
+ <item>14</item>
+ <item>15</item>
+ <item>16</item>
+ <item>17</item>
+ <item>18</item>
+ <item>19</item>
+ <item>20</item>
+ <item>21</item>
+ <item>22</item>
+ <item>23</item>
+ <item>24</item>
+ <item>25</item>
+ <item>26</item>
+ <item>27</item>
+ <item>28</item>
+ <item>29</item>
+ <item>30</item>
+ <item>31</item>
+ <item>32</item>
+ <item>33</item>
+ <item>34</item>
+ <item>35</item>
+ <item>36</item>
+ <item>37</item>
+ <item>38</item>
+ <item>39</item>
+ </string-array>
<string name="night_mode">DAY</string>
diff --git a/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java b/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java
index 86886bd..1033e46 100644
--- a/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java
+++ b/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java
@@ -1015,12 +1015,6 @@
}
@Override
- @SuppressLint("SyntheticAccessor")
- public void show() {
- showPopup();
- }
-
- @Override
public void show(int textDirection, int textAlignment) {
final boolean wasShowing = isShowing();
diff --git a/buildSrc/src/main/kotlin/androidx/build/DiffAndDocs.kt b/buildSrc/src/main/kotlin/androidx/build/DiffAndDocs.kt
index 4c8706c..8c6efff 100644
--- a/buildSrc/src/main/kotlin/androidx/build/DiffAndDocs.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/DiffAndDocs.kt
@@ -567,6 +567,7 @@
): TaskProvider<GenerateDocsTask> =
project.tasks.register(taskName, GenerateDocsTask::class.java) {
it.apply {
+ exclude("**/R.java")
dependsOn(generateSdkApiTask, doclavaConfig)
group = JavaBasePlugin.DOCUMENTATION_GROUP
description = "Generates Java documentation in the style of d.android.com. To generate offline " +
diff --git a/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt b/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt
index 60e0de9..aac2591 100644
--- a/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt
@@ -65,6 +65,7 @@
ignore(LibraryGroups.LIFECYCLE.group, "lifecycle-livedata-core-ktx")
ignore(LibraryGroups.LIFECYCLE.group, "lifecycle-compiler")
ignore(LibraryGroups.LIFECYCLE.group, "lifecycle-common-eap")
+ ignore(LibraryGroups.LIFECYCLE.group, "lifecycle-runtime-eap")
prebuilts(LibraryGroups.LIFECYCLE, "lifecycle-viewmodel-savedstate", "1.0.0-alpha01")
prebuilts(LibraryGroups.LIFECYCLE, "2.1.0-alpha03")
prebuilts(LibraryGroups.LOADER, "1.1.0-beta01")
@@ -77,7 +78,7 @@
prebuilts(LibraryGroups.MEDIA2, "1.0.0-alpha03")
prebuilts(LibraryGroups.MEDIAROUTER, "1.1.0-alpha02")
ignore(LibraryGroups.NAVIGATION.group, "navigation-testing")
- prebuilts(LibraryGroups.NAVIGATION, "2.0.0-rc02")
+ prebuilts(LibraryGroups.NAVIGATION, "2.1.0-alpha01")
prebuilts(LibraryGroups.PAGING, "2.1.0")
prebuilts(LibraryGroups.PALETTE, "1.0.0")
prebuilts(LibraryGroups.PERCENTLAYOUT, "1.0.0")
@@ -89,6 +90,7 @@
prebuilts(LibraryGroups.RECYCLERVIEW, "recyclerview", "1.1.0-alpha03")
prebuilts(LibraryGroups.RECYCLERVIEW, "recyclerview-selection", "1.1.0-alpha01")
prebuilts(LibraryGroups.REMOTECALLBACK, "1.0.0-alpha01")
+ ignore(LibraryGroups.ROOM.group, "room-common-java8")
prebuilts(LibraryGroups.ROOM, "2.1.0-alpha05")
prebuilts(LibraryGroups.SAVEDSTATE, "1.0.0-alpha02")
prebuilts(LibraryGroups.SHARETARGET, "1.0.0-alpha01")
@@ -112,7 +114,7 @@
prebuilts(LibraryGroups.WEAR, "1.0.0")
.addStubs("wear/wear_stubs/com.google.android.wearable-stubs.jar")
prebuilts(LibraryGroups.WEBKIT, "1.0.0")
- prebuilts(LibraryGroups.WORKMANAGER, "2.0.0-rc01")
+ prebuilts(LibraryGroups.WORKMANAGER, "2.0.0")
default(Ignore)
}
diff --git a/car/core/api/1.0.0-alpha7.txt b/car/core/api/1.0.0-alpha7.txt
index a5d0f09..4e2813c 100644
--- a/car/core/api/1.0.0-alpha7.txt
+++ b/car/core/api/1.0.0-alpha7.txt
@@ -567,6 +567,7 @@
method public void setEnabled(boolean);
method public void setPrimaryActionEmptyIcon();
method public void setPrimaryActionIcon(android.graphics.drawable.Icon, int);
+ method public void setPrimaryActionIcon(android.graphics.drawable.Drawable, int);
method public void setPrimaryActionNoIcon();
method public void setShowSwitchDivider(boolean);
method public void setSwitchOnCheckedChangeListener(android.widget.CompoundButton.OnCheckedChangeListener?);
diff --git a/car/core/api/current.txt b/car/core/api/current.txt
index a5d0f09..4e2813c 100644
--- a/car/core/api/current.txt
+++ b/car/core/api/current.txt
@@ -567,6 +567,7 @@
method public void setEnabled(boolean);
method public void setPrimaryActionEmptyIcon();
method public void setPrimaryActionIcon(android.graphics.drawable.Icon, int);
+ method public void setPrimaryActionIcon(android.graphics.drawable.Drawable, int);
method public void setPrimaryActionNoIcon();
method public void setShowSwitchDivider(boolean);
method public void setSwitchOnCheckedChangeListener(android.widget.CompoundButton.OnCheckedChangeListener?);
diff --git a/car/core/src/androidTest/java/androidx/car/widget/SwitchListItemTest.java b/car/core/src/androidTest/java/androidx/car/widget/SwitchListItemTest.java
index cd842bd..ee19dda 100644
--- a/car/core/src/androidTest/java/androidx/car/widget/SwitchListItemTest.java
+++ b/car/core/src/androidTest/java/androidx/car/widget/SwitchListItemTest.java
@@ -474,7 +474,7 @@
}
@Test
- public void testSetPrimaryActionIcon() {
+ public void testSetPrimaryActionIcon_withIcon() {
SwitchListItem item = new SwitchListItem(mActivity);
item.setPrimaryActionIcon(
Icon.createWithResource(mActivity, android.R.drawable.sym_def_app_icon),
@@ -487,6 +487,19 @@
}
@Test
+ public void testSetPrimaryActionIcon_withDrawable() {
+ SwitchListItem item = new SwitchListItem(mActivity);
+ item.setPrimaryActionIcon(
+ mActivity.getDrawable(android.R.drawable.sym_def_app_icon),
+ SwitchListItem.PRIMARY_ACTION_ICON_SIZE_LARGE);
+
+ List<SwitchListItem> items = Arrays.asList(item);
+ setupPagedListView(items);
+
+ assertThat(getViewHolderAtPosition(0).getPrimaryIcon().getDrawable(), is(notNullValue()));
+ }
+
+ @Test
public void testPrimaryIconSizesInIncreasingOrder() {
SwitchListItem small = new SwitchListItem(mActivity);
small.setPrimaryActionIcon(
diff --git a/car/core/src/main/java/androidx/car/widget/SwitchListItem.java b/car/core/src/main/java/androidx/car/widget/SwitchListItem.java
index d0ae858..d34820d 100644
--- a/car/core/src/main/java/androidx/car/widget/SwitchListItem.java
+++ b/car/core/src/main/java/androidx/car/widget/SwitchListItem.java
@@ -20,6 +20,7 @@
import android.content.Context;
import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Handler;
import android.os.Looper;
@@ -114,6 +115,7 @@
@PrimaryActionType private int mPrimaryActionType = PRIMARY_ACTION_TYPE_NO_ICON;
private Icon mPrimaryActionIcon;
+ private Drawable mPrimaryActionIconDrawable;
@PrimaryActionIconSize private int mPrimaryActionIconSize = PRIMARY_ACTION_ICON_SIZE_SMALL;
private CharSequence mTitle;
@@ -202,6 +204,9 @@
/**
* Sets {@code Primary Action} to be represented by an icon.
*
+ * <p>If both this method and {@link #setPrimaryActionIcon(Drawable,int)} are called, then
+ * this method will take precedence.
+ *
* @param icon An icon to set as primary action.
* @param size small/medium/large. Available as {@link #PRIMARY_ACTION_ICON_SIZE_SMALL},
* {@link #PRIMARY_ACTION_ICON_SIZE_MEDIUM},
@@ -215,6 +220,24 @@
}
/**
+ * Sets {@code Primary Action} to be represented by an icon.
+ *
+ * <p>If both this method and {@link #setPrimaryActionIcon(Icon,int)} are called, then
+ * the other method will take precedence.
+ *
+ * @param drawable the Drawable to set.
+ * @param size small/medium/large. Available as {@link #PRIMARY_ACTION_ICON_SIZE_SMALL},
+ * {@link #PRIMARY_ACTION_ICON_SIZE_MEDIUM},
+ * {@link #PRIMARY_ACTION_ICON_SIZE_LARGE}.
+ */
+ public void setPrimaryActionIcon(@NonNull Drawable drawable, @PrimaryActionIconSize int size) {
+ mPrimaryActionType = PRIMARY_ACTION_TYPE_ICON;
+ mPrimaryActionIconDrawable = drawable;
+ mPrimaryActionIconSize = size;
+ markDirty();
+ }
+
+ /**
* Sets {@code Primary Action} to be empty icon.
*
* <p>{@code Text} would have a start margin as if {@code Primary Action} were set to primary
@@ -324,9 +347,13 @@
case PRIMARY_ACTION_TYPE_ICON:
mBinders.add(vh -> {
vh.getPrimaryIcon().setVisibility(View.VISIBLE);
- mPrimaryActionIcon.loadDrawableAsync(getContext(),
- drawable -> vh.getPrimaryIcon().setImageDrawable(drawable),
- new Handler(Looper.getMainLooper()));
+ if (mPrimaryActionIcon != null) {
+ mPrimaryActionIcon.loadDrawableAsync(getContext(),
+ drawable -> vh.getPrimaryIcon().setImageDrawable(drawable),
+ new Handler(Looper.getMainLooper()));
+ } else {
+ vh.getPrimaryIcon().setImageDrawable(mPrimaryActionIconDrawable);
+ }
});
break;
case PRIMARY_ACTION_TYPE_EMPTY_ICON:
diff --git a/core/api/1.1.0-alpha06.txt b/core/api/1.1.0-alpha06.txt
index 0ece2e3..84939d0 100644
--- a/core/api/1.1.0-alpha06.txt
+++ b/core/api/1.1.0-alpha06.txt
@@ -633,10 +633,9 @@
method public androidx.core.app.Person.Builder setUri(String?);
}
- public final class RemoteActionCompat {
+ public final class RemoteActionCompat implements androidx.versionedparcelable.VersionedParcelable {
ctor public RemoteActionCompat(androidx.core.graphics.drawable.IconCompat, CharSequence, CharSequence, android.app.PendingIntent);
ctor public RemoteActionCompat(androidx.core.app.RemoteActionCompat);
- method public static androidx.core.app.RemoteActionCompat createFromBundle(android.os.Bundle);
method @RequiresApi(26) public static androidx.core.app.RemoteActionCompat createFromRemoteAction(android.app.RemoteAction);
method public android.app.PendingIntent getActionIntent();
method public CharSequence getContentDescription();
@@ -646,7 +645,6 @@
method public void setEnabled(boolean);
method public void setShouldShowIcon(boolean);
method public boolean shouldShowIcon();
- method public android.os.Bundle toBundle();
method @RequiresApi(26) public android.app.RemoteAction toRemoteAction();
}
diff --git a/core/api/current.txt b/core/api/current.txt
index 0ece2e3..84939d0 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -633,10 +633,9 @@
method public androidx.core.app.Person.Builder setUri(String?);
}
- public final class RemoteActionCompat {
+ public final class RemoteActionCompat implements androidx.versionedparcelable.VersionedParcelable {
ctor public RemoteActionCompat(androidx.core.graphics.drawable.IconCompat, CharSequence, CharSequence, android.app.PendingIntent);
ctor public RemoteActionCompat(androidx.core.app.RemoteActionCompat);
- method public static androidx.core.app.RemoteActionCompat createFromBundle(android.os.Bundle);
method @RequiresApi(26) public static androidx.core.app.RemoteActionCompat createFromRemoteAction(android.app.RemoteAction);
method public android.app.PendingIntent getActionIntent();
method public CharSequence getContentDescription();
@@ -646,7 +645,6 @@
method public void setEnabled(boolean);
method public void setShouldShowIcon(boolean);
method public boolean shouldShowIcon();
- method public android.os.Bundle toBundle();
method @RequiresApi(26) public android.app.RemoteAction toRemoteAction();
}
diff --git a/core/src/androidTest/java/androidx/core/app/RemoteActionCompatTest.java b/core/src/androidTest/java/androidx/core/app/RemoteActionCompatTest.java
index 47da629..a299a46 100644
--- a/core/src/androidTest/java/androidx/core/app/RemoteActionCompatTest.java
+++ b/core/src/androidTest/java/androidx/core/app/RemoteActionCompatTest.java
@@ -21,65 +21,65 @@
import android.app.PendingIntent;
import android.content.Intent;
+import android.os.Parcel;
import android.support.v4.BaseInstrumentationTestCase;
import androidx.core.graphics.drawable.IconCompat;
-import androidx.test.core.app.ApplicationProvider;
+import androidx.test.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import androidx.versionedparcelable.ParcelUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
+
@RunWith(AndroidJUnit4.class)
@SmallTest
public class RemoteActionCompatTest extends BaseInstrumentationTestCase<TestActivity> {
+ private static final IconCompat ICON = IconCompat.createWithContentUri("content://test");
+ private static final String TITLE = "title";
+ private static final String DESCRIPTION = "description";
+ private static final PendingIntent ACTION = PendingIntent.getBroadcast(
+ InstrumentationRegistry.getContext(), 0, new Intent("TESTACTION"), 0);
public RemoteActionCompatTest() {
super(TestActivity.class);
}
@Test
- public void testRemoteAction_bundle() throws Throwable {
- IconCompat icon = IconCompat.createWithContentUri("content://test");
- String title = "title";
- String description = "description";
- PendingIntent action = PendingIntent.getBroadcast(
- ApplicationProvider.getApplicationContext(), 0,
- new Intent("TESTACTION"), 0);
- RemoteActionCompat reference = new RemoteActionCompat(icon, title, description, action);
- reference.setEnabled(false);
- reference.setShouldShowIcon(false);
-
- RemoteActionCompat result = RemoteActionCompat.createFromBundle(reference.toBundle());
-
- assertEquals(icon.getUri(), result.getIcon().getUri());
- assertEquals(title, result.getTitle());
- assertEquals(description, result.getContentDescription());
- assertEquals(action.getTargetPackage(), result.getActionIntent().getTargetPackage());
- assertFalse(result.isEnabled());
- assertFalse(result.shouldShowIcon());
+ public void testRemoteAction_shallowCopy() throws Throwable {
+ RemoteActionCompat reference = createTestRemoteActionCompat();
+ RemoteActionCompat result = new RemoteActionCompat(reference);
+ assertEqualsToTestRemoteActionCompat(result);
}
@Test
- public void testRemoteAction_shallowCopy() throws Throwable {
- IconCompat icon = IconCompat.createWithContentUri("content://test");
- String title = "title";
- String description = "description";
- PendingIntent action = PendingIntent.getBroadcast(
- ApplicationProvider.getApplicationContext(), 0,
- new Intent("TESTACTION"), 0);
- RemoteActionCompat reference = new RemoteActionCompat(icon, title, description, action);
+ public void testRemoteAction_parcel() {
+ RemoteActionCompat reference = createTestRemoteActionCompat();
+
+ Parcel p = Parcel.obtain();
+ p.writeParcelable(ParcelUtils.toParcelable(reference), 0);
+ p.setDataPosition(0);
+ RemoteActionCompat result = ParcelUtils.fromParcelable(
+ p.readParcelable(getClass().getClassLoader()));
+
+ assertEqualsToTestRemoteActionCompat(result);
+ }
+
+ private RemoteActionCompat createTestRemoteActionCompat() {
+ RemoteActionCompat reference = new RemoteActionCompat(ICON, TITLE, DESCRIPTION, ACTION);
reference.setEnabled(false);
reference.setShouldShowIcon(false);
-
- RemoteActionCompat result = new RemoteActionCompat(reference);
-
- assertEquals(icon.getUri(), result.getIcon().getUri());
- assertEquals(title, result.getTitle());
- assertEquals(description, result.getContentDescription());
- assertEquals(action.getTargetPackage(), result.getActionIntent().getTargetPackage());
- assertFalse(result.isEnabled());
- assertFalse(result.shouldShowIcon());
+ return reference;
}
-}
+
+ private void assertEqualsToTestRemoteActionCompat(RemoteActionCompat remoteAction) {
+ assertEquals(ICON.getUri(), remoteAction.getIcon().getUri());
+ assertEquals(TITLE, remoteAction.getTitle());
+ assertEquals(DESCRIPTION, remoteAction.getContentDescription());
+ assertEquals(ACTION.getTargetPackage(), remoteAction.getActionIntent().getTargetPackage());
+ assertFalse(remoteAction.isEnabled());
+ assertFalse(remoteAction.shouldShowIcon());
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/java/androidx/core/app/RemoteActionCompat.java b/core/src/main/java/androidx/core/app/RemoteActionCompat.java
index faf676c..28f89c7 100644
--- a/core/src/main/java/androidx/core/app/RemoteActionCompat.java
+++ b/core/src/main/java/androidx/core/app/RemoteActionCompat.java
@@ -19,12 +19,14 @@
import android.app.PendingIntent;
import android.app.RemoteAction;
import android.os.Build;
-import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.graphics.drawable.IconCompat;
import androidx.core.util.Preconditions;
+import androidx.versionedparcelable.ParcelField;
+import androidx.versionedparcelable.VersionedParcelable;
+import androidx.versionedparcelable.VersionedParcelize;
/**
* Represents a remote action that can be called from another process. The action can have an
@@ -32,21 +34,20 @@
* <p>
* This is a backward-compatible version of {@link RemoteAction}.
*/
-public final class RemoteActionCompat {
-
- private static final String EXTRA_ICON = "icon";
- private static final String EXTRA_TITLE = "title";
- private static final String EXTRA_CONTENT_DESCRIPTION = "desc";
- private static final String EXTRA_ACTION_INTENT = "action";
- private static final String EXTRA_ENABLED = "enabled";
- private static final String EXTRA_SHOULD_SHOW_ICON = "showicon";
-
- private final IconCompat mIcon;
- private final CharSequence mTitle;
- private final CharSequence mContentDescription;
- private final PendingIntent mActionIntent;
- private boolean mEnabled;
- private boolean mShouldShowIcon;
+@VersionedParcelize(jetifyAs = "android.support.v4.app.RemoteActionCompat")
+public final class RemoteActionCompat implements VersionedParcelable {
+ @ParcelField(1)
+ IconCompat mIcon;
+ @ParcelField(2)
+ CharSequence mTitle;
+ @ParcelField(3)
+ CharSequence mContentDescription;
+ @ParcelField(4)
+ PendingIntent mActionIntent;
+ @ParcelField(5)
+ boolean mEnabled;
+ @ParcelField(6)
+ boolean mShouldShowIcon;
public RemoteActionCompat(@NonNull IconCompat icon, @NonNull CharSequence title,
@NonNull CharSequence contentDescription, @NonNull PendingIntent intent) {
@@ -59,6 +60,11 @@
}
/**
+ * Used for VersionedParcelable.
+ */
+ RemoteActionCompat() {}
+
+ /**
* Constructs a {@link RemoteActionCompat} using data from {@code other}.
*/
public RemoteActionCompat(@NonNull RemoteActionCompat other) {
@@ -160,35 +166,4 @@
}
return action;
}
-
- /**
- * Converts this into a Bundle that can be converted back to a {@link RemoteActionCompat}
- * by calling {@link #createFromBundle(Bundle)}.
- */
- @NonNull
- public Bundle toBundle() {
- Bundle bundle = new Bundle();
- bundle.putBundle(EXTRA_ICON, mIcon.toBundle());
- bundle.putCharSequence(EXTRA_TITLE, mTitle);
- bundle.putCharSequence(EXTRA_CONTENT_DESCRIPTION, mContentDescription);
- bundle.putParcelable(EXTRA_ACTION_INTENT, mActionIntent);
- bundle.putBoolean(EXTRA_ENABLED, mEnabled);
- bundle.putBoolean(EXTRA_SHOULD_SHOW_ICON, mShouldShowIcon);
- return bundle;
- }
-
- /**
- * Converts the bundle created by {@link #toBundle()} back to {@link RemoteActionCompat}.
- */
- @NonNull
- public static RemoteActionCompat createFromBundle(@NonNull Bundle bundle) {
- RemoteActionCompat action = new RemoteActionCompat(
- IconCompat.createFromBundle(bundle.getBundle(EXTRA_ICON)),
- bundle.getCharSequence(EXTRA_TITLE),
- bundle.getCharSequence(EXTRA_CONTENT_DESCRIPTION),
- bundle.<PendingIntent>getParcelable(EXTRA_ACTION_INTENT));
- action.setEnabled(bundle.getBoolean(EXTRA_ENABLED));
- action.setShouldShowIcon(bundle.getBoolean(EXTRA_SHOULD_SHOW_ICON));
- return action;
- }
}
diff --git a/core/src/main/java/androidx/core/graphics/TypefaceCompatUtil.java b/core/src/main/java/androidx/core/graphics/TypefaceCompatUtil.java
index 1e6ca0e..8bf3e30 100644
--- a/core/src/main/java/androidx/core/graphics/TypefaceCompatUtil.java
+++ b/core/src/main/java/androidx/core/graphics/TypefaceCompatUtil.java
@@ -60,9 +60,14 @@
*/
@Nullable
public static File getTempFile(Context context) {
+ File cacheDir = context.getCacheDir();
+ if (cacheDir == null) {
+ return null;
+ }
+
final String prefix = CACHE_FILE_PREFIX + Process.myPid() + "-" + Process.myTid() + "-";
for (int i = 0; i < 100; ++i) {
- final File file = new File(context.getCacheDir(), prefix + i);
+ final File file = new File(cacheDir, prefix + i);
try {
if (file.createNewFile()) {
return file;
diff --git a/fragment/api/1.1.0-alpha06.txt b/fragment/api/1.1.0-alpha06.txt
index e586a2f..1c78cb8 100644
--- a/fragment/api/1.1.0-alpha06.txt
+++ b/fragment/api/1.1.0-alpha06.txt
@@ -331,14 +331,13 @@
method public boolean isViewFromObject(android.view.View, Object);
}
- public class FragmentTabHost extends android.widget.TabHost implements android.widget.TabHost.OnTabChangeListener {
- ctor public FragmentTabHost(android.content.Context);
- ctor public FragmentTabHost(android.content.Context, android.util.AttributeSet?);
- method public void addTab(android.widget.TabHost.TabSpec, Class<?>, android.os.Bundle?);
- method public void onTabChanged(String?);
- method @Deprecated public void setup();
- method public void setup(android.content.Context, androidx.fragment.app.FragmentManager);
- method public void setup(android.content.Context, androidx.fragment.app.FragmentManager, int);
+ @Deprecated public class FragmentTabHost extends android.widget.TabHost implements android.widget.TabHost.OnTabChangeListener {
+ ctor @Deprecated public FragmentTabHost(android.content.Context);
+ ctor @Deprecated public FragmentTabHost(android.content.Context, android.util.AttributeSet?);
+ method @Deprecated public void addTab(android.widget.TabHost.TabSpec, Class<?>, android.os.Bundle?);
+ method @Deprecated public void onTabChanged(String?);
+ method @Deprecated public void setup(android.content.Context, androidx.fragment.app.FragmentManager);
+ method @Deprecated public void setup(android.content.Context, androidx.fragment.app.FragmentManager, int);
}
public abstract class FragmentTransaction {
diff --git a/fragment/api/current.txt b/fragment/api/current.txt
index e586a2f..1c78cb8 100644
--- a/fragment/api/current.txt
+++ b/fragment/api/current.txt
@@ -331,14 +331,13 @@
method public boolean isViewFromObject(android.view.View, Object);
}
- public class FragmentTabHost extends android.widget.TabHost implements android.widget.TabHost.OnTabChangeListener {
- ctor public FragmentTabHost(android.content.Context);
- ctor public FragmentTabHost(android.content.Context, android.util.AttributeSet?);
- method public void addTab(android.widget.TabHost.TabSpec, Class<?>, android.os.Bundle?);
- method public void onTabChanged(String?);
- method @Deprecated public void setup();
- method public void setup(android.content.Context, androidx.fragment.app.FragmentManager);
- method public void setup(android.content.Context, androidx.fragment.app.FragmentManager, int);
+ @Deprecated public class FragmentTabHost extends android.widget.TabHost implements android.widget.TabHost.OnTabChangeListener {
+ ctor @Deprecated public FragmentTabHost(android.content.Context);
+ ctor @Deprecated public FragmentTabHost(android.content.Context, android.util.AttributeSet?);
+ method @Deprecated public void addTab(android.widget.TabHost.TabSpec, Class<?>, android.os.Bundle?);
+ method @Deprecated public void onTabChanged(String?);
+ method @Deprecated public void setup(android.content.Context, androidx.fragment.app.FragmentManager);
+ method @Deprecated public void setup(android.content.Context, androidx.fragment.app.FragmentManager, int);
}
public abstract class FragmentTransaction {
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/CountCallsFragment.java b/fragment/src/androidTest/java/androidx/fragment/app/CountCallsFragment.java
deleted file mode 100644
index 900ec9d..0000000
--- a/fragment/src/androidTest/java/androidx/fragment/app/CountCallsFragment.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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 androidx.fragment.app;
-
-import android.content.Context;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * Counts the number of onCreateView, onHiddenChanged (onHide, onShow), onAttach, and onDetach
- * calls.
- */
-public class CountCallsFragment extends StrictViewFragment {
- public int onCreateViewCount = 0;
- public int onDestroyViewCount = 0;
- public int onHideCount = 0;
- public int onShowCount = 0;
- public int onAttachCount = 0;
- public int onDetachCount = 0;
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- onCreateViewCount++;
- return super.onCreateView(inflater, container, savedInstanceState);
- }
-
- @Override
- public void onHiddenChanged(boolean hidden) {
- if (hidden) {
- onHideCount++;
- } else {
- onShowCount++;
- }
- super.onHiddenChanged(hidden);
- }
-
- @Override
- public void onAttach(Context context) {
- onAttachCount++;
- super.onAttach(context);
- }
-
- @Override
- public void onDetach() {
- onDetachCount++;
- super.onDetach();
- }
-
- @Override
- public void onDestroyView() {
- onDestroyViewCount++;
- super.onDestroyView();
- }
-}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/CountCallsFragment.kt b/fragment/src/androidTest/java/androidx/fragment/app/CountCallsFragment.kt
new file mode 100644
index 0000000..e1e3378
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/CountCallsFragment.kt
@@ -0,0 +1,72 @@
+/*
+ * 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 androidx.fragment.app
+
+import android.content.Context
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.LayoutRes
+import androidx.fragment.test.R
+
+/**
+ * Counts the number of onCreateView, onHiddenChanged (onHide, onShow), onAttach, and onDetach
+ * calls.
+ */
+class CountCallsFragment(
+ @LayoutRes contentLayoutId: Int = R.layout.strict_view_fragment
+) : StrictViewFragment(contentLayoutId) {
+ var onCreateViewCount = 0
+ var onDestroyViewCount = 0
+ var onHideCount = 0
+ var onShowCount = 0
+ var onAttachCount = 0
+ var onDetachCount = 0
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ onCreateViewCount++
+ return super.onCreateView(inflater, container, savedInstanceState)
+ }
+
+ override fun onHiddenChanged(hidden: Boolean) {
+ if (hidden) {
+ onHideCount++
+ } else {
+ onShowCount++
+ }
+ super.onHiddenChanged(hidden)
+ }
+
+ override fun onAttach(context: Context) {
+ onAttachCount++
+ super.onAttach(context)
+ }
+
+ override fun onDetach() {
+ onDetachCount++
+ super.onDetach()
+ }
+
+ override fun onDestroyView() {
+ onDestroyViewCount++
+ super.onDestroyView()
+ }
+}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimationTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimationTest.kt
index e4a3be9..300fad6 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimationTest.kt
+++ b/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimationTest.kt
@@ -355,8 +355,7 @@
val fm1 = fc1.supportFragmentManager
- val fragment1 = StrictViewFragment()
- fragment1.setLayoutId(R.layout.scene1)
+ val fragment1 = StrictViewFragment(R.layout.scene1)
fm1.beginTransaction()
.add(R.id.fragmentContainer, fragment1, "1")
.commit()
@@ -596,7 +595,7 @@
@Throws(InterruptedException::class)
private fun assertPostponed(fragment: AnimatorFragment, expectedAnimators: Int) {
- assertThat(fragment.mOnCreateViewCalled).isTrue()
+ assertThat(fragment.onCreateViewCalled).isTrue()
assertThat(fragment.requireView().visibility).isEqualTo(View.VISIBLE)
assertThat(fragment.requireView().alpha).isWithin(0f).of(0f)
assertThat(fragment.numAnimators).isEqualTo(expectedAnimators)
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatorTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatorTest.kt
index b96e10b..b71b846 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatorTest.kt
+++ b/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatorTest.kt
@@ -415,8 +415,7 @@
val fm1 = fc1.supportFragmentManager
- val fragment1 = StrictViewFragment()
- fragment1.setLayoutId(R.layout.scene1)
+ val fragment1 = StrictViewFragment(R.layout.scene1)
fm1.beginTransaction()
.add(R.id.fragmentContainer, fragment1, "1")
.setReorderingAllowed(true)
@@ -509,7 +508,7 @@
}
private fun assertPostponed(fragment: AnimatorFragment, expectedAnimators: Int) {
- assertThat(fragment.mOnCreateViewCalled).isTrue()
+ assertThat(fragment.onCreateViewCalled).isTrue()
assertThat(fragment.requireView().visibility).isEqualTo(View.VISIBLE)
assertThat(fragment.requireView().alpha).isWithin(0f).of(0f)
assertThat(fragment.numAnimators).isEqualTo(expectedAnimators)
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.java b/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.java
deleted file mode 100644
index a52ef71..0000000
--- a/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.java
+++ /dev/null
@@ -1,2038 +0,0 @@
-/*
- * 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 androidx.fragment.app;
-
-import static androidx.fragment.app.FragmentTestUtil.HostCallbacks;
-import static androidx.fragment.app.FragmentTestUtil.restartFragmentController;
-import static androidx.fragment.app.FragmentTestUtil.shutdownFragmentController;
-import static androidx.fragment.app.FragmentTestUtil.startupFragmentController;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNotSame;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Parcelable;
-import android.util.AttributeSet;
-import android.util.Pair;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import androidx.annotation.ContentView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.view.ViewCompat;
-import androidx.fragment.app.test.EmptyFragmentTestActivity;
-import androidx.fragment.app.test.FragmentTestActivity;
-import androidx.fragment.test.R;
-import androidx.lifecycle.Lifecycle;
-import androidx.lifecycle.ViewModelStore;
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.MediumTest;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.rule.ActivityTestRule;
-
-import org.junit.Assert;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.TimeUnit;
-
-@RunWith(AndroidJUnit4.class)
-@MediumTest
-public class FragmentLifecycleTest {
-
- @Rule
- public ActivityTestRule<EmptyFragmentTestActivity> mActivityRule =
- new ActivityTestRule<EmptyFragmentTestActivity>(EmptyFragmentTestActivity.class);
-
- @Test
- public void basicLifecycle() throws Throwable {
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictFragment strictFragment = new StrictFragment();
-
- // Add fragment; StrictFragment will throw if it detects any violation
- // in standard lifecycle method ordering or expected preconditions.
- fm.beginTransaction().add(strictFragment, "EmptyHeadless").commit();
- executePendingTransactions(fm);
-
- assertTrue("fragment is not added", strictFragment.isAdded());
- assertFalse("fragment is detached", strictFragment.isDetached());
- assertTrue("fragment is not resumed", strictFragment.isResumed());
- Lifecycle lifecycle = strictFragment.getLifecycle();
- assertThat(lifecycle.getCurrentState())
- .isEqualTo(Lifecycle.State.RESUMED);
-
- // Test removal as well; StrictFragment will throw here too.
- fm.beginTransaction().remove(strictFragment).commit();
- executePendingTransactions(fm);
-
- assertFalse("fragment is added", strictFragment.isAdded());
- assertFalse("fragment is resumed", strictFragment.isResumed());
- assertThat(lifecycle.getCurrentState())
- .isEqualTo(Lifecycle.State.DESTROYED);
- // Once removed, a new Lifecycle should be created just in case
- // the developer reuses the same Fragment
- assertThat(strictFragment.getLifecycle().getCurrentState())
- .isEqualTo(Lifecycle.State.INITIALIZED);
-
- // This one is perhaps counterintuitive; "detached" means specifically detached
- // but still managed by a FragmentManager. The .remove call above
- // should not enter this state.
- assertFalse("fragment is detached", strictFragment.isDetached());
- }
-
- @Test
- public void detachment() throws Throwable {
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictFragment f1 = new StrictFragment();
- final StrictFragment f2 = new StrictFragment();
-
- fm.beginTransaction().add(f1, "1").add(f2, "2").commit();
- executePendingTransactions(fm);
-
- assertTrue("fragment 1 is not added", f1.isAdded());
- assertTrue("fragment 2 is not added", f2.isAdded());
-
- // Test detaching fragments using StrictFragment to throw on errors.
- fm.beginTransaction().detach(f1).detach(f2).commit();
- executePendingTransactions(fm);
-
- assertTrue("fragment 1 is not detached", f1.isDetached());
- assertTrue("fragment 2 is not detached", f2.isDetached());
- assertFalse("fragment 1 is added", f1.isAdded());
- assertFalse("fragment 2 is added", f2.isAdded());
-
- // Only reattach f1; leave v2 detached.
- fm.beginTransaction().attach(f1).commit();
- executePendingTransactions(fm);
-
- assertTrue("fragment 1 is not added", f1.isAdded());
- assertFalse("fragment 1 is detached", f1.isDetached());
- assertTrue("fragment 2 is not detached", f2.isDetached());
-
- // Remove both from the FragmentManager.
- fm.beginTransaction().remove(f1).remove(f2).commit();
- executePendingTransactions(fm);
-
- assertFalse("fragment 1 is added", f1.isAdded());
- assertFalse("fragment 2 is added", f2.isAdded());
- assertFalse("fragment 1 is detached", f1.isDetached());
- assertFalse("fragment 2 is detached", f2.isDetached());
- }
-
- @Test
- public void basicBackStack() throws Throwable {
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictFragment f1 = new StrictFragment();
- final StrictFragment f2 = new StrictFragment();
-
- // Add a fragment normally to set up
- fm.beginTransaction().add(f1, "1").commit();
- executePendingTransactions(fm);
-
- assertTrue("fragment 1 is not added", f1.isAdded());
-
- // Remove the first one and add a second. We're not using replace() here since
- // these fragments are headless and as of this test writing, replace() only works
- // for fragments with views and a container view id.
- // Add it to the back stack so we can pop it afterwards.
- fm.beginTransaction().remove(f1).add(f2, "2").addToBackStack("stack1").commit();
- executePendingTransactions(fm);
-
- assertFalse("fragment 1 is added", f1.isAdded());
- assertTrue("fragment 2 is not added", f2.isAdded());
-
- // Test popping the stack
- fm.popBackStack();
- executePendingTransactions(fm);
-
- assertFalse("fragment 2 is added", f2.isAdded());
- assertTrue("fragment 1 is not added", f1.isAdded());
- }
-
- @Test
- public void attachBackStack() throws Throwable {
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictFragment f1 = new StrictFragment();
- final StrictFragment f2 = new StrictFragment();
-
- // Add a fragment normally to set up
- fm.beginTransaction().add(f1, "1").commit();
- executePendingTransactions(fm);
-
- assertTrue("fragment 1 is not added", f1.isAdded());
-
- fm.beginTransaction().detach(f1).add(f2, "2").addToBackStack("stack1").commit();
- executePendingTransactions(fm);
-
- assertTrue("fragment 1 is not detached", f1.isDetached());
- assertFalse("fragment 2 is detached", f2.isDetached());
- assertFalse("fragment 1 is added", f1.isAdded());
- assertTrue("fragment 2 is not added", f2.isAdded());
- }
-
- @Test
- public void viewLifecycle() throws Throwable {
- // Test basic lifecycle when the fragment creates a view
-
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictViewFragment f1 = new StrictViewFragment();
-
- fm.beginTransaction().add(android.R.id.content, f1).commit();
- executePendingTransactions(fm);
-
- assertTrue("fragment 1 is not added", f1.isAdded());
- final View view = f1.getView();
- assertNotNull("fragment 1 returned null from getView", view);
- assertTrue("fragment 1's view is not attached to a window",
- ViewCompat.isAttachedToWindow(view));
-
- fm.beginTransaction().remove(f1).commit();
- executePendingTransactions(fm);
-
- assertFalse("fragment 1 is added", f1.isAdded());
- assertNull("fragment 1 returned non-null from getView after removal", f1.getView());
- assertFalse("fragment 1's previous view is still attached to a window",
- ViewCompat.isAttachedToWindow(view));
- }
-
- @Test
- public void viewReplace() throws Throwable {
- // Replace one view with another, then reverse it with the back stack
-
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictViewFragment f1 = new StrictViewFragment();
- final StrictViewFragment f2 = new StrictViewFragment();
-
- fm.beginTransaction().add(android.R.id.content, f1).commit();
- executePendingTransactions(fm);
-
- assertTrue("fragment 1 is not added", f1.isAdded());
-
- View origView1 = f1.getView();
- assertNotNull("fragment 1 returned null view", origView1);
- assertTrue("fragment 1's view not attached", ViewCompat.isAttachedToWindow(origView1));
-
- fm.beginTransaction().replace(android.R.id.content, f2).addToBackStack("stack1").commit();
- executePendingTransactions(fm);
-
- assertFalse("fragment 1 is added", f1.isAdded());
- assertTrue("fragment 2 is added", f2.isAdded());
- assertNull("fragment 1 returned non-null view", f1.getView());
- assertFalse("fragment 1's old view still attached",
- ViewCompat.isAttachedToWindow(origView1));
- View origView2 = f2.getView();
- assertNotNull("fragment 2 returned null view", origView2);
- assertTrue("fragment 2's view not attached", ViewCompat.isAttachedToWindow(origView2));
-
- fm.popBackStack();
- executePendingTransactions(fm);
-
- assertTrue("fragment 1 is not added", f1.isAdded());
- assertFalse("fragment 2 is added", f2.isAdded());
- assertNull("fragment 2 returned non-null view", f2.getView());
- assertFalse("fragment 2's view still attached", ViewCompat.isAttachedToWindow(origView2));
- View newView1 = f1.getView();
- assertNotSame("fragment 1 had same view from last attachment", origView1, newView1);
- assertTrue("fragment 1's view not attached", ViewCompat.isAttachedToWindow(newView1));
- }
-
- @Test
- @UiThreadTest
- public void setInitialSavedState() throws Throwable {
- FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
-
- // Add a StateSaveFragment
- StateSaveFragment fragment = new StateSaveFragment("Saved", "");
- fm.beginTransaction().add(fragment, "tag").commit();
- executePendingTransactions(fm);
-
- // Change the user visible hint before we save state
- fragment.setUserVisibleHint(false);
-
- // Save its state and remove it
- Fragment.SavedState state = fm.saveFragmentInstanceState(fragment);
- fm.beginTransaction().remove(fragment).commit();
- executePendingTransactions(fm);
-
- // Create a new instance, calling setInitialSavedState
- fragment = new StateSaveFragment("", "");
- fragment.setInitialSavedState(state);
-
- // Add the new instance
- fm.beginTransaction().add(fragment, "tag").commit();
- executePendingTransactions(fm);
-
- assertEquals("setInitialSavedState did not restore saved state",
- "Saved", fragment.getSavedState());
- assertEquals("setInitialSavedState did not restore user visible hint",
- false, fragment.getUserVisibleHint());
- }
-
- @Test
- @UiThreadTest
- public void setInitialSavedStateWithSetUserVisibleHint() throws Throwable {
- FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
-
- // Add a StateSaveFragment
- StateSaveFragment fragment = new StateSaveFragment("Saved", "");
- fm.beginTransaction().add(fragment, "tag").commit();
- executePendingTransactions(fm);
-
- // Save its state and remove it
- Fragment.SavedState state = fm.saveFragmentInstanceState(fragment);
- fm.beginTransaction().remove(fragment).commit();
- executePendingTransactions(fm);
-
- // Create a new instance, calling setInitialSavedState
- fragment = new StateSaveFragment("", "");
- fragment.setInitialSavedState(state);
-
- // Change the user visible hint after we call setInitialSavedState
- fragment.setUserVisibleHint(false);
-
- // Add the new instance
- fm.beginTransaction().add(fragment, "tag").commit();
- executePendingTransactions(fm);
-
- assertEquals("setInitialSavedState did not restore saved state",
- "Saved", fragment.getSavedState());
- assertEquals("setUserVisibleHint should override setInitialSavedState",
- false, fragment.getUserVisibleHint());
- }
-
- @Test
- @UiThreadTest
- public void testSavedInstanceStateAfterRestore() {
-
- final ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc1 =
- startupFragmentController(mActivityRule.getActivity(), null, viewModelStore);
- final FragmentManager fm1 = fc1.getSupportFragmentManager();
-
- // Add the initial state
- final StrictFragment parentFragment = new StrictFragment();
- parentFragment.setRetainInstance(true);
- final StrictFragment childFragment = new StrictFragment();
- fm1.beginTransaction().add(parentFragment, "parent").commitNow();
- final FragmentManager childFragmentManager = parentFragment.getChildFragmentManager();
- childFragmentManager.beginTransaction().add(childFragment, "child").commitNow();
-
- // Confirm the initial state
- assertWithMessage("Initial parent saved instance state should be null")
- .that(parentFragment.mSavedInstanceState)
- .isNull();
- assertWithMessage("Initial child saved instance state should be null")
- .that(childFragment.mSavedInstanceState)
- .isNull();
-
- // Bring the state back down to destroyed, simulating an activity restart
- fc1.dispatchPause();
- final Parcelable savedState = fc1.saveAllState();
- fc1.dispatchStop();
- fc1.dispatchDestroy();
-
- // Create the new controller and restore state
- final FragmentController fc2 =
- startupFragmentController(mActivityRule.getActivity(), savedState, viewModelStore);
- final FragmentManager fm2 = fc2.getSupportFragmentManager();
-
- final StrictFragment restoredParentFragment = (StrictFragment) fm2
- .findFragmentByTag("parent");
- assertNotNull("Parent fragment was not restored", restoredParentFragment);
- final StrictFragment restoredChildFragment = (StrictFragment) restoredParentFragment
- .getChildFragmentManager().findFragmentByTag("child");
- assertNotNull("Child fragment was not restored", restoredChildFragment);
-
- assertWithMessage("Parent fragment saved instance state should still be null "
- + "since it is a retained Fragment")
- .that(restoredParentFragment.mSavedInstanceState)
- .isNull();
- assertWithMessage("Child fragment saved instance state should be non-null")
- .that(restoredChildFragment.mSavedInstanceState)
- .isNotNull();
-
- // Bring the state back down to destroyed before we finish the test
- shutdownFragmentController(fc2, viewModelStore);
- }
-
- @Test
- @UiThreadTest
- public void restoreNestedFragmentsOnBackStack() {
-
- final ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc1 = FragmentController.createController(
- new HostCallbacks(mActivityRule.getActivity(), viewModelStore));
-
- final FragmentManager fm1 = fc1.getSupportFragmentManager();
-
- fc1.attachHost(null);
- fc1.dispatchCreate();
-
- // Add the initial state
- final StrictFragment parentFragment = new StrictFragment();
- final StrictFragment childFragment = new StrictFragment();
- fm1.beginTransaction().add(parentFragment, "parent").commitNow();
- final FragmentManager childFragmentManager = parentFragment.getChildFragmentManager();
- childFragmentManager.beginTransaction().add(childFragment, "child").commitNow();
-
- // Now add a Fragment to the back stack
- final StrictFragment replacementChildFragment = new StrictFragment();
- childFragmentManager.beginTransaction()
- .remove(childFragment)
- .add(replacementChildFragment, "child")
- .addToBackStack("back_stack").commit();
- childFragmentManager.executePendingTransactions();
-
- // Move the activity to resumed
- fc1.dispatchActivityCreated();
- fc1.noteStateNotSaved();
- fc1.execPendingActions();
- fc1.dispatchStart();
- fc1.dispatchResume();
- fc1.execPendingActions();
-
- // Now bring the state back down
- fc1.dispatchPause();
- final Parcelable savedState = fc1.saveAllState();
- fc1.dispatchStop();
- fc1.dispatchDestroy();
-
- // Create the new controller and restore state
- final FragmentController fc2 = FragmentController.createController(
- new HostCallbacks(mActivityRule.getActivity(), viewModelStore));
-
- final FragmentManager fm2 = fc2.getSupportFragmentManager();
-
- fc2.attachHost(null);
- fc2.restoreSaveState(savedState);
- fc2.dispatchCreate();
-
- final StrictFragment restoredParentFragment = (StrictFragment) fm2
- .findFragmentByTag("parent");
- assertNotNull("Parent fragment was not restored", restoredParentFragment);
- final StrictFragment restoredChildFragment = (StrictFragment) restoredParentFragment
- .getChildFragmentManager().findFragmentByTag("child");
- assertNotNull("Child fragment was not restored", restoredChildFragment);
-
- fc2.dispatchActivityCreated();
- fc2.noteStateNotSaved();
- fc2.execPendingActions();
- fc2.dispatchStart();
- fc2.dispatchResume();
- fc2.execPendingActions();
-
- // Bring the state back down to destroyed before we finish the test
- shutdownFragmentController(fc2, viewModelStore);
- }
-
- @Test
- @UiThreadTest
- public void restoreRetainedInstanceFragments() throws Throwable {
- // Create a new FragmentManager in isolation, nest some assorted fragments
- // and then restore them to a second new FragmentManager.
-
- final ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc1 = FragmentController.createController(
- new HostCallbacks(mActivityRule.getActivity(), viewModelStore));
-
- final FragmentManager fm1 = fc1.getSupportFragmentManager();
-
- fc1.attachHost(null);
- fc1.dispatchCreate();
-
- // Configure fragments.
-
- // This retained fragment will be added, then removed. After being removed, it
- // should no longer be retained by the FragmentManager
- final StateSaveFragment removedFragment = new StateSaveFragment("Removed",
- "UnsavedRemoved");
- removedFragment.setRetainInstance(true);
- fm1.beginTransaction().add(removedFragment, "tag:removed").commitNow();
- fm1.beginTransaction().remove(removedFragment).commitNow();
-
- // This retained fragment will be added, then detached. After being detached, it
- // should continue to be retained by the FragmentManager
- final StateSaveFragment detachedFragment = new StateSaveFragment("Detached",
- "UnsavedDetached");
- removedFragment.setRetainInstance(true);
- fm1.beginTransaction().add(detachedFragment, "tag:detached").commitNow();
- fm1.beginTransaction().detach(detachedFragment).commitNow();
-
- // Grandparent fragment will not retain instance
- final StateSaveFragment grandparentFragment = new StateSaveFragment("Grandparent",
- "UnsavedGrandparent");
- assertNotNull("grandparent fragment saved state not initialized",
- grandparentFragment.getSavedState());
- assertNotNull("grandparent fragment unsaved state not initialized",
- grandparentFragment.getUnsavedState());
- fm1.beginTransaction().add(grandparentFragment, "tag:grandparent").commitNow();
-
- // Parent fragment will retain instance
- final StateSaveFragment parentFragment = new StateSaveFragment("Parent", "UnsavedParent");
- assertNotNull("parent fragment saved state not initialized",
- parentFragment.getSavedState());
- assertNotNull("parent fragment unsaved state not initialized",
- parentFragment.getUnsavedState());
- parentFragment.setRetainInstance(true);
- grandparentFragment.getChildFragmentManager().beginTransaction()
- .add(parentFragment, "tag:parent").commitNow();
- assertSame("parent fragment is not a child of grandparent",
- grandparentFragment, parentFragment.getParentFragment());
-
- // Child fragment will not retain instance
- final StateSaveFragment childFragment = new StateSaveFragment("Child", "UnsavedChild");
- assertNotNull("child fragment saved state not initialized",
- childFragment.getSavedState());
- assertNotNull("child fragment unsaved state not initialized",
- childFragment.getUnsavedState());
- parentFragment.getChildFragmentManager().beginTransaction()
- .add(childFragment, "tag:child").commitNow();
- assertSame("child fragment is not a child of grandpanret",
- parentFragment, childFragment.getParentFragment());
-
- // Saved for comparison later
- final FragmentManager parentChildFragmentManager = parentFragment.getChildFragmentManager();
-
- fc1.dispatchActivityCreated();
- fc1.noteStateNotSaved();
- fc1.execPendingActions();
- fc1.dispatchStart();
- fc1.dispatchResume();
- fc1.execPendingActions();
-
- // Bring the state back down to destroyed, simulating an activity restart
- fc1.dispatchPause();
- final Parcelable savedState = fc1.saveAllState();
- fc1.dispatchStop();
- fc1.dispatchDestroy();
-
- // Create the new controller and restore state
- final FragmentController fc2 = FragmentController.createController(
- new HostCallbacks(mActivityRule.getActivity(), viewModelStore));
-
- final FragmentManager fm2 = fc2.getSupportFragmentManager();
-
- fc2.attachHost(null);
- fc2.restoreSaveState(savedState);
- fc2.dispatchCreate();
-
- // Confirm that the restored fragments are available and in the expected states
- final StateSaveFragment restoredRemovedFragment = (StateSaveFragment)
- fm2.findFragmentByTag("tag:removed");
- assertNull(restoredRemovedFragment);
- assertTrue("Removed Fragment should be destroyed", removedFragment.mCalledOnDestroy);
-
- final StateSaveFragment restoredDetachedFragment = (StateSaveFragment)
- fm2.findFragmentByTag("tag:detached");
- assertNotNull(restoredDetachedFragment);
-
- final StateSaveFragment restoredGrandparent = (StateSaveFragment) fm2.findFragmentByTag(
- "tag:grandparent");
- assertNotNull("grandparent fragment not restored", restoredGrandparent);
-
- assertNotSame("grandparent fragment instance was saved",
- grandparentFragment, restoredGrandparent);
- assertEquals("grandparent fragment saved state was not equal",
- grandparentFragment.getSavedState(), restoredGrandparent.getSavedState());
- assertNotEquals("grandparent fragment unsaved state was unexpectedly preserved",
- grandparentFragment.getUnsavedState(), restoredGrandparent.getUnsavedState());
-
- final StateSaveFragment restoredParent = (StateSaveFragment) restoredGrandparent
- .getChildFragmentManager().findFragmentByTag("tag:parent");
- assertNotNull("parent fragment not restored", restoredParent);
-
- assertSame("parent fragment instance was not saved", parentFragment, restoredParent);
- assertEquals("parent fragment saved state was not equal",
- parentFragment.getSavedState(), restoredParent.getSavedState());
- assertEquals("parent fragment unsaved state was not equal",
- parentFragment.getUnsavedState(), restoredParent.getUnsavedState());
- assertNotSame("parent fragment has the same child FragmentManager",
- parentChildFragmentManager, restoredParent.getChildFragmentManager());
-
- final StateSaveFragment restoredChild = (StateSaveFragment) restoredParent
- .getChildFragmentManager().findFragmentByTag("tag:child");
- assertNotNull("child fragment not restored", restoredChild);
-
- assertNotSame("child fragment instance state was saved", childFragment, restoredChild);
- assertEquals("child fragment saved state was not equal",
- childFragment.getSavedState(), restoredChild.getSavedState());
- assertNotEquals("child fragment saved state was unexpectedly equal",
- childFragment.getUnsavedState(), restoredChild.getUnsavedState());
-
- fc2.dispatchActivityCreated();
- fc2.noteStateNotSaved();
- fc2.execPendingActions();
- fc2.dispatchStart();
- fc2.dispatchResume();
- fc2.execPendingActions();
-
- // Test that the fragments are in the configuration we expect
-
- // Bring the state back down to destroyed before we finish the test
- shutdownFragmentController(fc2, viewModelStore);
-
- assertTrue("grandparent not destroyed", restoredGrandparent.mCalledOnDestroy);
- assertTrue("parent not destroyed", restoredParent.mCalledOnDestroy);
- assertTrue("child not destroyed", restoredChild.mCalledOnDestroy);
- }
-
- @Test
- @UiThreadTest
- public void restoreRetainedInstanceFragmentWithTransparentActivityConfigChange() {
- // Create a new FragmentManager in isolation, add a retained instance Fragment,
- // then mimic the following scenario:
- // 1. Activity A adds retained Fragment F
- // 2. Activity A starts translucent Activity B
- // 3. Activity B start opaque Activity C
- // 4. Rotate phone
- // 5. Finish Activity C
- // 6. Finish Activity B
-
- final ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc1 = FragmentController.createController(
- new HostCallbacks(mActivityRule.getActivity(), viewModelStore));
-
- final FragmentManager fm1 = fc1.getSupportFragmentManager();
-
- fc1.attachHost(null);
- fc1.dispatchCreate();
-
- // Add the retained Fragment
- final StateSaveFragment retainedFragment = new StateSaveFragment("Retained",
- "UnsavedRetained");
- retainedFragment.setRetainInstance(true);
- fm1.beginTransaction().add(retainedFragment, "tag:retained").commitNow();
-
- // Move the activity to resumed
- fc1.dispatchActivityCreated();
- fc1.noteStateNotSaved();
- fc1.execPendingActions();
- fc1.dispatchStart();
- fc1.dispatchResume();
- fc1.execPendingActions();
-
- // Launch the transparent activity on top
- fc1.dispatchPause();
-
- // Launch the opaque activity on top
- final Parcelable savedState = fc1.saveAllState();
- fc1.dispatchStop();
-
- // Finish the opaque activity, making our Activity visible i.e., started
- fc1.noteStateNotSaved();
- fc1.execPendingActions();
- fc1.dispatchStart();
-
- // Finish the transparent activity, causing a config change
- fc1.dispatchStop();
- fc1.dispatchDestroy();
-
- // Create the new controller and restore state
- final FragmentController fc2 = FragmentController.createController(
- new HostCallbacks(mActivityRule.getActivity(), viewModelStore));
-
- final FragmentManager fm2 = fc2.getSupportFragmentManager();
-
- fc2.attachHost(null);
- fc2.restoreSaveState(savedState);
- fc2.dispatchCreate();
-
- final StateSaveFragment restoredFragment = (StateSaveFragment) fm2
- .findFragmentByTag("tag:retained");
- assertNotNull("retained fragment not restored", restoredFragment);
- assertEquals("The retained Fragment shouldn't be recreated",
- retainedFragment, restoredFragment);
-
- fc2.dispatchActivityCreated();
- fc2.noteStateNotSaved();
- fc2.execPendingActions();
- fc2.dispatchStart();
- fc2.dispatchResume();
- fc2.execPendingActions();
-
- // Bring the state back down to destroyed before we finish the test
- shutdownFragmentController(fc2, viewModelStore);
- }
-
- @Test
- @UiThreadTest
- public void saveAnimationState() throws Throwable {
- ViewModelStore viewModelStore = new ViewModelStore();
- FragmentController fc = startupFragmentController(mActivityRule.getActivity(), null,
- viewModelStore);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- fm.beginTransaction()
- .setCustomAnimations(0, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
- .add(android.R.id.content, SimpleFragment.create(R.layout.fragment_a))
- .addToBackStack(null)
- .commit();
- fm.executePendingTransactions();
-
- assertAnimationsMatch(fm, 0, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out);
-
- // Causes save and restore of fragments and back stack
- fc = restartFragmentController(mActivityRule.getActivity(), fc, viewModelStore);
- fm = fc.getSupportFragmentManager();
-
- assertAnimationsMatch(fm, 0, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out);
-
- fm.beginTransaction()
- .setCustomAnimations(R.anim.fade_in, R.anim.fade_out, 0, 0)
- .replace(android.R.id.content, SimpleFragment.create(R.layout.fragment_b))
- .addToBackStack(null)
- .commit();
- fm.executePendingTransactions();
-
- assertAnimationsMatch(fm, R.anim.fade_in, R.anim.fade_out, 0, 0);
-
- // Causes save and restore of fragments and back stack
- fc = restartFragmentController(mActivityRule.getActivity(), fc, viewModelStore);
- fm = fc.getSupportFragmentManager();
-
- assertAnimationsMatch(fm, R.anim.fade_in, R.anim.fade_out, 0, 0);
-
- fm.popBackStackImmediate();
-
- assertAnimationsMatch(fm, 0, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out);
-
- shutdownFragmentController(fc, viewModelStore);
- }
-
- /**
- * This test confirms that as long as a parent fragment has called super.onCreate,
- * any child fragments added, committed and with transactions executed will be brought
- * to at least the CREATED state by the time the parent fragment receives onCreateView.
- * This means the child fragment will have received onAttach/onCreate.
- */
- @Test
- @UiThreadTest
- public void childFragmentManagerAttach() throws Throwable {
- final ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc = FragmentController.createController(
- new HostCallbacks(mActivityRule.getActivity(), viewModelStore));
- fc.attachHost(null);
- fc.dispatchCreate();
-
- FragmentManager.FragmentLifecycleCallbacks
- mockLc = mock(FragmentManager.FragmentLifecycleCallbacks.class);
- FragmentManager.FragmentLifecycleCallbacks
- mockRecursiveLc = mock(FragmentManager.FragmentLifecycleCallbacks.class);
-
- FragmentManager fm = fc.getSupportFragmentManager();
- fm.registerFragmentLifecycleCallbacks(mockLc, false);
- fm.registerFragmentLifecycleCallbacks(mockRecursiveLc, true);
-
- ChildFragmentManagerFragment fragment = new ChildFragmentManagerFragment();
- fm.beginTransaction()
- .add(android.R.id.content, fragment)
- .commitNow();
-
- verify(mockLc, times(1)).onFragmentCreated(fm, fragment, null);
-
- fc.dispatchActivityCreated();
-
- Fragment childFragment = fragment.getChildFragment();
-
- verify(mockLc, times(1)).onFragmentActivityCreated(fm, fragment, null);
- verify(mockRecursiveLc, times(1)).onFragmentActivityCreated(fm, fragment, null);
- verify(mockRecursiveLc, times(1)).onFragmentActivityCreated(fm, childFragment, null);
-
- fc.dispatchStart();
-
- verify(mockLc, times(1)).onFragmentStarted(fm, fragment);
- verify(mockRecursiveLc, times(1)).onFragmentStarted(fm, fragment);
- verify(mockRecursiveLc, times(1)).onFragmentStarted(fm, childFragment);
-
- fc.dispatchResume();
-
- verify(mockLc, times(1)).onFragmentResumed(fm, fragment);
- verify(mockRecursiveLc, times(1)).onFragmentResumed(fm, fragment);
- verify(mockRecursiveLc, times(1)).onFragmentResumed(fm, childFragment);
-
- // Confirm that the parent fragment received onAttachFragment
- assertTrue("parent fragment did not receive onAttachFragment",
- fragment.mCalledOnAttachFragment);
-
- fc.dispatchStop();
-
- verify(mockLc, times(1)).onFragmentStopped(fm, fragment);
- verify(mockRecursiveLc, times(1)).onFragmentStopped(fm, fragment);
- verify(mockRecursiveLc, times(1)).onFragmentStopped(fm, childFragment);
-
- viewModelStore.clear();
- fc.dispatchDestroy();
-
- verify(mockLc, times(1)).onFragmentDestroyed(fm, fragment);
- verify(mockRecursiveLc, times(1)).onFragmentDestroyed(fm, fragment);
- verify(mockRecursiveLc, times(1)).onFragmentDestroyed(fm, childFragment);
- }
-
- /**
- * This test checks that FragmentLifecycleCallbacks are invoked when expected.
- */
- @Test
- @UiThreadTest
- public void fragmentLifecycleCallbacks() throws Throwable {
- final ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc = FragmentController.createController(
- new HostCallbacks(mActivityRule.getActivity(), viewModelStore));
- fc.attachHost(null);
- fc.dispatchCreate();
-
- FragmentManager fm = fc.getSupportFragmentManager();
-
- ChildFragmentManagerFragment fragment = new ChildFragmentManagerFragment();
- fm.beginTransaction()
- .add(android.R.id.content, fragment)
- .commitNow();
-
- fc.dispatchActivityCreated();
-
- fc.dispatchStart();
- fc.dispatchResume();
-
- // Confirm that the parent fragment received onAttachFragment
- assertTrue("parent fragment did not receive onAttachFragment",
- fragment.mCalledOnAttachFragment);
-
- shutdownFragmentController(fc, viewModelStore);
- }
-
- /**
- * This tests that fragments call onDestroy when the activity finishes.
- */
- @Test
- @UiThreadTest
- public void fragmentDestroyedOnFinish() throws Throwable {
- ViewModelStore viewModelStore = new ViewModelStore();
- FragmentController fc = startupFragmentController(mActivityRule.getActivity(), null,
- viewModelStore);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- StrictViewFragment fragmentA = StrictViewFragment.create(R.layout.fragment_a);
- StrictViewFragment fragmentB = StrictViewFragment.create(R.layout.fragment_b);
- fm.beginTransaction()
- .add(android.R.id.content, fragmentA)
- .commit();
- fm.executePendingTransactions();
- fm.beginTransaction()
- .replace(android.R.id.content, fragmentB)
- .addToBackStack(null)
- .commit();
- fm.executePendingTransactions();
- shutdownFragmentController(fc, viewModelStore);
- assertTrue(fragmentB.mCalledOnDestroy);
- assertTrue(fragmentA.mCalledOnDestroy);
- }
-
- // Make sure that executing transactions during activity lifecycle events
- // is properly prevented.
- @Test
- public void preventReentrantCalls() throws Throwable {
- testLifecycleTransitionFailure(StrictFragment.ATTACHED, StrictFragment.CREATED);
- testLifecycleTransitionFailure(StrictFragment.CREATED, StrictFragment.ACTIVITY_CREATED);
- testLifecycleTransitionFailure(StrictFragment.ACTIVITY_CREATED, StrictFragment.STARTED);
- testLifecycleTransitionFailure(StrictFragment.STARTED, StrictFragment.RESUMED);
-
- testLifecycleTransitionFailure(StrictFragment.RESUMED, StrictFragment.STARTED);
- testLifecycleTransitionFailure(StrictFragment.STARTED, StrictFragment.CREATED);
- testLifecycleTransitionFailure(StrictFragment.CREATED, StrictFragment.ATTACHED);
- testLifecycleTransitionFailure(StrictFragment.ATTACHED, StrictFragment.DETACHED);
- }
-
- private void testLifecycleTransitionFailure(final int fromState,
- final int toState) throws Throwable {
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- final ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc1 = startupFragmentController(
- mActivityRule.getActivity(), null, viewModelStore);
-
- final FragmentManager fm1 = fc1.getSupportFragmentManager();
-
- final Fragment reentrantFragment = ReentrantFragment.create(fromState, toState);
-
- fm1.beginTransaction()
- .add(reentrantFragment, "reentrant")
- .commit();
- try {
- fm1.executePendingTransactions();
- } catch (IllegalStateException e) {
- fail("An exception shouldn't happen when initially adding the fragment");
- }
-
- // Now shut down the fragment controller. When fromState > toState, this should
- // result in an exception
- Parcelable savedState;
- try {
- fc1.dispatchPause();
- savedState = fc1.saveAllState();
- fc1.dispatchStop();
- fc1.dispatchDestroy();
- if (fromState > toState) {
- fail("Expected IllegalStateException when moving from "
- + StrictFragment.stateToString(fromState) + " to "
- + StrictFragment.stateToString(toState));
- }
- } catch (IllegalStateException e) {
- if (fromState < toState) {
- fail("Unexpected IllegalStateException when moving from "
- + StrictFragment.stateToString(fromState) + " to "
- + StrictFragment.stateToString(toState));
- }
- return; // test passed!
- }
-
- // now restore from saved state. This will be reached when
- // fromState < toState. We want to catch the fragment while it
- // is being restored as the fragment controller state is being brought up.
-
- try {
- startupFragmentController(mActivityRule.getActivity(), savedState,
- viewModelStore);
-
- fail("Expected IllegalStateException when moving from "
- + StrictFragment.stateToString(fromState) + " to "
- + StrictFragment.stateToString(toState));
- } catch (IllegalStateException e) {
- // expected, so the test passed!
- }
- }
- });
- }
-
- /**
- * Test to ensure that when dispatch* is called that the fragment manager
- * doesn't cause the contained fragment states to change even if no state changes.
- */
- @Test
- @UiThreadTest
- public void noPrematureStateChange() throws Throwable {
- ViewModelStore viewModelStore = new ViewModelStore();
- FragmentController fc = startupFragmentController(mActivityRule.getActivity(), null,
- viewModelStore);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- fm.beginTransaction()
- .add(new StrictFragment(), "1")
- .commitNow();
-
- fc = restartFragmentController(mActivityRule.getActivity(), fc, viewModelStore);
-
- fm = fc.getSupportFragmentManager();
-
- StrictFragment fragment1 = (StrictFragment) fm.findFragmentByTag("1");
- assertWithMessage("Fragment should be resumed after restart")
- .that(fragment1.mCalledOnResume)
- .isTrue();
- fragment1.mCalledOnResume = false;
- fc.dispatchResume();
-
- assertWithMessage("Fragment should not get onResume() after second dispatchResume()")
- .that(fragment1.mCalledOnResume)
- .isFalse();
- }
-
- @Test
- @UiThreadTest
- public void testIsStateSaved() throws Throwable {
- ViewModelStore viewModelStore = new ViewModelStore();
- FragmentController fc = startupFragmentController(mActivityRule.getActivity(), null,
- viewModelStore);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- Fragment f = new StrictFragment();
- fm.beginTransaction()
- .add(f, "1")
- .commitNow();
-
- assertFalse("fragment reported state saved while resumed", f.isStateSaved());
-
- fc.dispatchPause();
- fc.saveAllState();
-
- assertTrue("fragment reported state not saved after saveAllState", f.isStateSaved());
-
- fc.dispatchStop();
-
- assertTrue("fragment reported state not saved after stop", f.isStateSaved());
-
- viewModelStore.clear();
- fc.dispatchDestroy();
-
- assertFalse("fragment reported state saved after destroy", f.isStateSaved());
- }
-
- @Test
- @UiThreadTest
- public void testSetArgumentsLifecycle() throws Throwable {
- ViewModelStore viewModelStore = new ViewModelStore();
- FragmentController fc = startupFragmentController(mActivityRule.getActivity(), null,
- viewModelStore);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- Fragment f = new StrictFragment();
- f.setArguments(new Bundle());
-
- fm.beginTransaction()
- .add(f, "1")
- .commitNow();
-
- f.setArguments(new Bundle());
-
- fc.dispatchPause();
- fc.saveAllState();
-
- boolean threw = false;
- try {
- f.setArguments(new Bundle());
- } catch (IllegalStateException ise) {
- threw = true;
- }
- assertTrue("fragment allowed setArguments after state save", threw);
-
- fc.dispatchStop();
-
- threw = false;
- try {
- f.setArguments(new Bundle());
- } catch (IllegalStateException ise) {
- threw = true;
- }
- assertTrue("fragment allowed setArguments after stop", threw);
-
- viewModelStore.clear();
- fc.dispatchDestroy();
-
- // Fully destroyed, so fragments have been removed.
- f.setArguments(new Bundle());
- }
-
- /*
- * Test that target fragments are in a useful state when we restore them, even if they're
- * on the back stack.
- */
-
- @Test
- @UiThreadTest
- public void targetFragmentRestoreLifecycleStateBackStack() throws Throwable {
- ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc1 = FragmentController.createController(
- new HostCallbacks(mActivityRule.getActivity(), viewModelStore));
-
- final FragmentManager fm1 = fc1.getSupportFragmentManager();
-
- fc1.attachHost(null);
- fc1.dispatchCreate();
-
- final Fragment target = new TargetFragment();
- fm1.beginTransaction().add(target, "target").commitNow();
-
- final Fragment referrer = new ReferrerFragment();
- referrer.setTargetFragment(target, 0);
-
- fm1.beginTransaction()
- .remove(target)
- .add(referrer, "referrer")
- .addToBackStack(null)
- .commit();
-
- fc1.dispatchActivityCreated();
- fc1.noteStateNotSaved();
- fc1.execPendingActions();
- fc1.dispatchStart();
- fc1.dispatchResume();
- fc1.execPendingActions();
-
- // Simulate an activity restart
- final FragmentController fc2 =
- restartFragmentController(mActivityRule.getActivity(), fc1, viewModelStore);
-
- // Bring the state back down to destroyed before we finish the test
- shutdownFragmentController(fc2, viewModelStore);
- }
-
- @Test
- @UiThreadTest
- public void targetFragmentRestoreLifecycleStateManagerOrder() throws Throwable {
- ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc1 = FragmentController.createController(
- new HostCallbacks(mActivityRule.getActivity(), viewModelStore));
-
- final FragmentManager fm1 = fc1.getSupportFragmentManager();
-
- fc1.attachHost(null);
- fc1.dispatchCreate();
-
- final Fragment target1 = new TargetFragment();
- final Fragment referrer1 = new ReferrerFragment();
- referrer1.setTargetFragment(target1, 0);
-
- fm1.beginTransaction().add(target1, "target1").add(referrer1, "referrer1").commitNow();
-
- final Fragment target2 = new TargetFragment();
- final Fragment referrer2 = new ReferrerFragment();
- referrer2.setTargetFragment(target2, 0);
-
- // Order shouldn't matter.
- fm1.beginTransaction().add(referrer2, "referrer2").add(target2, "target2").commitNow();
-
- fc1.dispatchActivityCreated();
- fc1.noteStateNotSaved();
- fc1.execPendingActions();
- fc1.dispatchStart();
- fc1.dispatchResume();
- fc1.execPendingActions();
-
- // Simulate an activity restart
- final FragmentController fc2 =
- restartFragmentController(mActivityRule.getActivity(), fc1, viewModelStore);
-
- // Bring the state back down to destroyed before we finish the test
- shutdownFragmentController(fc2, viewModelStore);
- }
-
- @Test
- @UiThreadTest
- public void targetFragmentClearedWhenSetToNull() {
- ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc =
- startupFragmentController(mActivityRule.getActivity(), null, viewModelStore);
-
- final FragmentManager fm = fc.getSupportFragmentManager();
-
- final Fragment target = new TargetFragment();
- final Fragment referrer = new ReferrerFragment();
- referrer.setTargetFragment(target, 0);
-
- assertWithMessage("Target Fragment should be accessible before being added")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- fm.beginTransaction().add(target, "target").add(referrer, "referrer").commitNow();
-
- assertWithMessage("Target Fragment should be accessible after being added")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- referrer.setTargetFragment(null, 0);
-
- assertWithMessage("Target Fragment should cleared after setTargetFragment with null")
- .that(referrer.getTargetFragment())
- .isNull();
-
- fm.beginTransaction()
- .remove(referrer)
- .commitNow();
-
- assertWithMessage("Target Fragment should still be cleared after being removed")
- .that(referrer.getTargetFragment())
- .isNull();
-
- shutdownFragmentController(fc, viewModelStore);
- }
-
- /**
- * Test the availability of getTargetFragment() when the target Fragment is already
- * attached to a FragmentManager, but the referrer Fragment is not attached.
- */
- @Test
- @UiThreadTest
- public void targetFragmentOnlyTargetAdded() {
- ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc =
- startupFragmentController(mActivityRule.getActivity(), null, viewModelStore);
-
- final FragmentManager fm = fc.getSupportFragmentManager();
-
- final Fragment target = new TargetFragment();
- // Add just the target Fragment to the FragmentManager
- fm.beginTransaction().add(target, "target").commitNow();
-
- final Fragment referrer = new ReferrerFragment();
- referrer.setTargetFragment(target, 0);
-
- assertWithMessage("Target Fragment should be accessible before being added")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- fm.beginTransaction().add(referrer, "referrer").commitNow();
-
- assertWithMessage("Target Fragment should be accessible after being added")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- fm.beginTransaction()
- .remove(referrer)
- .commitNow();
-
- assertWithMessage("Target Fragment should be accessible after being removed")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- shutdownFragmentController(fc, viewModelStore);
- }
-
- /**
- * Test the availability of getTargetFragment() when the target fragment is
- * not retained and the referrer fragment is not retained.
- */
- @Test
- @UiThreadTest
- public void targetFragmentNonRetainedNonRetained() {
- ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc =
- startupFragmentController(mActivityRule.getActivity(), null, viewModelStore);
-
- final FragmentManager fm = fc.getSupportFragmentManager();
-
- final Fragment target = new TargetFragment();
- final Fragment referrer = new ReferrerFragment();
- referrer.setTargetFragment(target, 0);
-
- assertWithMessage("Target Fragment should be accessible before being added")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- fm.beginTransaction().add(target, "target").add(referrer, "referrer").commitNow();
-
- assertWithMessage("Target Fragment should be accessible after being added")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- fm.beginTransaction()
- .remove(referrer)
- .commitNow();
-
- assertWithMessage("Target Fragment should be accessible after being removed")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- shutdownFragmentController(fc, viewModelStore);
-
- assertWithMessage("Target Fragment should be accessible after destruction")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
- }
-
- /**
- * Test the availability of getTargetFragment() when the target fragment is
- * retained and the referrer fragment is not retained.
- */
- @Test
- @UiThreadTest
- public void targetFragmentRetainedNonRetained() {
- ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc =
- startupFragmentController(mActivityRule.getActivity(), null, viewModelStore);
-
- final FragmentManager fm = fc.getSupportFragmentManager();
-
- final Fragment target = new TargetFragment();
- target.setRetainInstance(true);
- final Fragment referrer = new ReferrerFragment();
- referrer.setTargetFragment(target, 0);
-
- assertWithMessage("Target Fragment should be accessible before being added")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- fm.beginTransaction().add(target, "target").add(referrer, "referrer").commitNow();
-
- assertWithMessage("Target Fragment should be accessible after being added")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- fm.beginTransaction()
- .remove(referrer)
- .commitNow();
-
- assertWithMessage("Target Fragment should be accessible after being removed")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- shutdownFragmentController(fc, viewModelStore);
-
- assertWithMessage("Target Fragment should be accessible after destruction")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
- }
-
- /**
- * Test the availability of getTargetFragment() when the target fragment is
- * not retained and the referrer fragment is retained.
- */
- @Test
- @UiThreadTest
- public void targetFragmentNonRetainedRetained() {
- ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc =
- startupFragmentController(mActivityRule.getActivity(), null, viewModelStore);
-
- final FragmentManager fm = fc.getSupportFragmentManager();
-
- final Fragment target = new TargetFragment();
- final Fragment referrer = new ReferrerFragment();
- referrer.setTargetFragment(target, 0);
- referrer.setRetainInstance(true);
-
- assertWithMessage("Target Fragment should be accessible before being added")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- fm.beginTransaction().add(target, "target").add(referrer, "referrer").commitNow();
-
- assertWithMessage("Target Fragment should be accessible after being added")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- // Save the state
- fc.dispatchPause();
- fc.saveAllState();
- fc.dispatchStop();
- fc.dispatchDestroy();
-
- assertWithMessage("Target Fragment should be accessible after target Fragment destruction")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
- }
-
- /**
- * Test the availability of getTargetFragment() when the target fragment is
- * retained and the referrer fragment is also retained.
- */
- @Test
- @UiThreadTest
- public void targetFragmentRetainedRetained() {
- ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc =
- startupFragmentController(mActivityRule.getActivity(), null, viewModelStore);
-
- final FragmentManager fm = fc.getSupportFragmentManager();
-
- final Fragment target = new TargetFragment();
- target.setRetainInstance(true);
- final Fragment referrer = new ReferrerFragment();
- referrer.setRetainInstance(true);
- referrer.setTargetFragment(target, 0);
-
- assertWithMessage("Target Fragment should be accessible before being added")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- fm.beginTransaction().add(target, "target").add(referrer, "referrer").commitNow();
-
- assertWithMessage("Target Fragment should be accessible after being added")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- // Save the state
- fc.dispatchPause();
- fc.saveAllState();
- fc.dispatchStop();
- fc.dispatchDestroy();
-
- assertWithMessage("Target Fragment should be accessible after FragmentManager destruction")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
- }
-
- @Test
- public void targetFragmentNoCycles() throws Throwable {
- final Fragment one = new Fragment();
- final Fragment two = new Fragment();
- final Fragment three = new Fragment();
-
- try {
- one.setTargetFragment(two, 0);
- two.setTargetFragment(three, 0);
- three.setTargetFragment(one, 0);
- assertTrue("creating a fragment target cycle did not throw IllegalArgumentException",
- false);
- } catch (IllegalArgumentException e) {
- // Success!
- }
- }
-
- @Test
- public void targetFragmentSetClear() throws Throwable {
- final Fragment one = new Fragment();
- final Fragment two = new Fragment();
-
- one.setTargetFragment(two, 0);
- one.setTargetFragment(null, 0);
- }
-
- /**
- * FragmentActivity should not raise the state of a Fragment while it is being destroyed.
- */
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN_MR1)
- @Test
- public void fragmentActivityFinishEarly() throws Throwable {
- Intent intent = new Intent(mActivityRule.getActivity(), FragmentTestActivity.class);
- intent.putExtra("finishEarly", true);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-
- FragmentTestActivity activity = (FragmentTestActivity)
- InstrumentationRegistry.getInstrumentation().startActivitySync(intent);
-
- assertTrue(activity.onDestroyLatch.await(1000, TimeUnit.MILLISECONDS));
- }
-
- /**
- * When a fragment is saved in non-config, it should be restored to the same index.
- */
- @Test
- @UiThreadTest
- public void restoreNonConfig() throws Throwable {
- FragmentController fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, null);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- Fragment backStackRetainedFragment = new StrictFragment();
- backStackRetainedFragment.setRetainInstance(true);
- Fragment fragment1 = new StrictFragment();
- fm.beginTransaction()
- .add(backStackRetainedFragment, "backStack")
- .add(fragment1, "1")
- .setPrimaryNavigationFragment(fragment1)
- .addToBackStack(null)
- .commit();
- fm.executePendingTransactions();
- Fragment fragment2 = new StrictFragment();
- fragment2.setRetainInstance(true);
- fragment2.setTargetFragment(fragment1, 0);
- Fragment fragment3 = new StrictFragment();
- fm.beginTransaction()
- .remove(backStackRetainedFragment)
- .remove(fragment1)
- .add(fragment2, "2")
- .add(fragment3, "3")
- .addToBackStack(null)
- .commit();
- fm.executePendingTransactions();
-
- Pair<Parcelable, FragmentManagerNonConfig> savedState =
- FragmentTestUtil.destroy(mActivityRule, fc);
-
- fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, savedState);
- boolean foundFragment2 = false;
- for (Fragment fragment : fc.getSupportFragmentManager().getFragments()) {
- if (fragment == fragment2) {
- foundFragment2 = true;
- assertNotNull(fragment.getTargetFragment());
- assertEquals("1", fragment.getTargetFragment().getTag());
- } else {
- assertNotEquals("2", fragment.getTag());
- }
- }
- assertTrue(foundFragment2);
- fc.getSupportFragmentManager().popBackStackImmediate();
- Fragment foundBackStackRetainedFragment = fc.getSupportFragmentManager()
- .findFragmentByTag("backStack");
- assertEquals("Retained Fragment on the back stack was not retained",
- backStackRetainedFragment, foundBackStackRetainedFragment);
- }
-
- /**
- * Check that retained fragments in the backstack correctly restored after two "configChanges"
- */
- @Test
- @UiThreadTest
- public void retainedFragmentInBackstack() throws Throwable {
- FragmentController fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, null);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- Fragment fragment1 = new StrictFragment();
- fm.beginTransaction()
- .add(fragment1, "1")
- .addToBackStack(null)
- .commit();
- fm.executePendingTransactions();
-
- Fragment child = new StrictFragment();
- child.setRetainInstance(true);
- fragment1.getChildFragmentManager().beginTransaction()
- .add(child, "child").commit();
- fragment1.getChildFragmentManager().executePendingTransactions();
-
- Fragment fragment2 = new StrictFragment();
- fm.beginTransaction()
- .remove(fragment1)
- .add(fragment2, "2")
- .addToBackStack(null)
- .commit();
- fm.executePendingTransactions();
-
- Pair<Parcelable, FragmentManagerNonConfig> savedState =
- FragmentTestUtil.destroy(mActivityRule, fc);
-
- fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, savedState);
- savedState = FragmentTestUtil.destroy(mActivityRule, fc);
- fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, savedState);
- fm = fc.getSupportFragmentManager();
- fm.popBackStackImmediate();
- Fragment retainedChild = fm.findFragmentByTag("1")
- .getChildFragmentManager().findFragmentByTag("child");
- assertEquals(child, retainedChild);
- }
-
- /**
- * When a fragment has been optimized out, it state should still be saved during
- * save and restore instance state.
- */
- @Test
- @UiThreadTest
- public void saveRemovedFragment() throws Throwable {
- FragmentController fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, null);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- SaveStateFragment fragment1 = SaveStateFragment.create(1);
- fm.beginTransaction()
- .add(android.R.id.content, fragment1, "1")
- .addToBackStack(null)
- .commit();
- SaveStateFragment fragment2 = SaveStateFragment.create(2);
- fm.beginTransaction()
- .replace(android.R.id.content, fragment2, "2")
- .addToBackStack(null)
- .commit();
- fm.executePendingTransactions();
-
- Pair<Parcelable, FragmentManagerNonConfig> savedState =
- FragmentTestUtil.destroy(mActivityRule, fc);
-
- fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, savedState);
- fm = fc.getSupportFragmentManager();
- fragment2 = (SaveStateFragment) fm.findFragmentByTag("2");
- assertNotNull(fragment2);
- assertEquals(2, fragment2.getValue());
- fm.popBackStackImmediate();
- fragment1 = (SaveStateFragment) fm.findFragmentByTag("1");
- assertNotNull(fragment1);
- assertEquals(1, fragment1.getValue());
- }
-
- /**
- * When there are no retained instance fragments, the FragmentManagerNonConfig's fragments
- * should be null
- */
- @Test
- @UiThreadTest
- public void nullNonConfig() throws Throwable {
- FragmentController fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, null);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- Fragment fragment1 = new StrictFragment();
- fm.beginTransaction()
- .add(fragment1, "1")
- .addToBackStack(null)
- .commit();
- fm.executePendingTransactions();
- Pair<Parcelable, FragmentManagerNonConfig> savedState =
- FragmentTestUtil.destroy(mActivityRule, fc);
- assertNull(savedState.second);
- }
-
- /**
- * When the FragmentManager state changes, the pending transactions should execute.
- */
- @Test
- @UiThreadTest
- public void runTransactionsOnChange() throws Throwable {
- FragmentController fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, null);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- RemoveHelloInOnResume fragment1 = new RemoveHelloInOnResume();
- StrictFragment fragment2 = new StrictFragment();
- fm.beginTransaction()
- .add(fragment1, "1")
- .setReorderingAllowed(false)
- .commit();
- fm.beginTransaction()
- .add(fragment2, "Hello")
- .setReorderingAllowed(false)
- .commit();
- fm.executePendingTransactions();
-
- assertEquals(2, fm.getFragments().size());
- assertTrue(fm.getFragments().contains(fragment1));
- assertTrue(fm.getFragments().contains(fragment2));
-
- Pair<Parcelable, FragmentManagerNonConfig> savedState =
- FragmentTestUtil.destroy(mActivityRule, fc);
- fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, savedState);
- fm = fc.getSupportFragmentManager();
-
- assertEquals(1, fm.getFragments().size());
- for (Fragment fragment : fm.getFragments()) {
- assertTrue(fragment instanceof RemoveHelloInOnResume);
- }
- }
-
- @Test
- @UiThreadTest
- public void optionsMenu() throws Throwable {
- FragmentController fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, null);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- InvalidateOptionFragment fragment = new InvalidateOptionFragment();
- fm.beginTransaction()
- .add(android.R.id.content, fragment)
- .commit();
- fm.executePendingTransactions();
-
- Menu menu = mock(Menu.class);
- fc.dispatchPrepareOptionsMenu(menu);
- assertTrue(fragment.onPrepareOptionsMenuCalled);
- fragment.onPrepareOptionsMenuCalled = false;
- FragmentTestUtil.destroy(mActivityRule, fc);
- fc.dispatchPrepareOptionsMenu(menu);
- assertFalse(fragment.onPrepareOptionsMenuCalled);
- }
-
- /**
- * When a retained instance fragment is saved while in the back stack, it should go
- * through onCreate() when it is popped back.
- */
- @Test
- @UiThreadTest
- public void retainInstanceWithOnCreate() throws Throwable {
- FragmentController fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, null);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- OnCreateFragment fragment1 = new OnCreateFragment();
-
- fm.beginTransaction()
- .add(fragment1, "1")
- .commit();
- fm.beginTransaction()
- .remove(fragment1)
- .addToBackStack(null)
- .commit();
-
- Pair<Parcelable, FragmentManagerNonConfig> savedState =
- FragmentTestUtil.destroy(mActivityRule, fc);
- Pair<Parcelable, FragmentManagerNonConfig> restartState =
- Pair.create(savedState.first, null);
-
- fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, restartState);
-
- // Save again, but keep the state
- savedState = FragmentTestUtil.destroy(mActivityRule, fc);
-
- fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, savedState);
-
- fm = fc.getSupportFragmentManager();
-
- fm.popBackStackImmediate();
- OnCreateFragment fragment2 = (OnCreateFragment) fm.findFragmentByTag("1");
- assertTrue(fragment2.onCreateCalled);
- fm.popBackStackImmediate();
- }
-
- /**
- * A retained instance fragment should go through onCreate() once, even through save and
- * restore.
- */
- @Test
- @UiThreadTest
- public void retainInstanceOneOnCreate() throws Throwable {
- FragmentController fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, null);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- OnCreateFragment fragment = new OnCreateFragment();
-
- fm.beginTransaction()
- .add(fragment, "fragment")
- .commit();
- fm.executePendingTransactions();
-
- fm.beginTransaction()
- .remove(fragment)
- .addToBackStack(null)
- .commit();
-
- assertTrue(fragment.onCreateCalled);
- fragment.onCreateCalled = false;
-
- Pair<Parcelable, FragmentManagerNonConfig> savedState =
- FragmentTestUtil.destroy(mActivityRule, fc);
-
- fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, savedState);
- fm = fc.getSupportFragmentManager();
-
- fm.popBackStackImmediate();
- assertFalse(fragment.onCreateCalled);
- }
-
- /**
- * A retained instance fragment added via XML should go through onCreate() once, but should get
- * onInflate calls for each inflation.
- */
- @Test
- @UiThreadTest
- public void retainInstanceLayoutOnInflate() throws Throwable {
- FragmentController fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, null);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- RetainedInflatedParentFragment parentFragment = new RetainedInflatedParentFragment();
-
- fm.beginTransaction()
- .add(android.R.id.content, parentFragment)
- .commit();
- fm.executePendingTransactions();
-
- RetainedInflatedChildFragment childFragment = (RetainedInflatedChildFragment)
- parentFragment.getChildFragmentManager().findFragmentById(R.id.child_fragment);
-
- fm.beginTransaction()
- .remove(parentFragment)
- .addToBackStack(null)
- .commit();
-
- Pair<Parcelable, FragmentManagerNonConfig> savedState =
- FragmentTestUtil.destroy(mActivityRule, fc);
-
- fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, savedState);
- fm = fc.getSupportFragmentManager();
-
- fm.popBackStackImmediate();
-
- parentFragment = (RetainedInflatedParentFragment) fm.findFragmentById(android.R.id.content);
- RetainedInflatedChildFragment childFragment2 = (RetainedInflatedChildFragment)
- parentFragment.getChildFragmentManager().findFragmentById(R.id.child_fragment);
-
- assertEquals("Child Fragment should be retained", childFragment, childFragment2);
- assertEquals("Child Fragment should have onInflate called twice",
- 2, childFragment2.mOnInflateCount);
- }
-
- private void assertAnimationsMatch(FragmentManager fm, int enter, int exit, int popEnter,
- int popExit) {
- FragmentManagerImpl fmImpl = (FragmentManagerImpl) fm;
- BackStackRecord record = fmImpl.mBackStack.get(fmImpl.mBackStack.size() - 1);
-
- Assert.assertEquals(enter, record.mEnterAnim);
- Assert.assertEquals(exit, record.mExitAnim);
- Assert.assertEquals(popEnter, record.mPopEnterAnim);
- Assert.assertEquals(popExit, record.mPopExitAnim);
- }
-
- private void executePendingTransactions(final FragmentManager fm) throws Throwable {
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- fm.executePendingTransactions();
- }
- });
- }
-
- public static class StateSaveFragment extends StrictFragment {
- private static final String STATE_KEY = "state";
-
- private String mSavedState;
- private String mUnsavedState;
-
- public StateSaveFragment() {
- }
-
- public StateSaveFragment(String savedState, String unsavedState) {
- mSavedState = savedState;
- mUnsavedState = unsavedState;
- }
-
- public String getSavedState() {
- return mSavedState;
- }
-
- public String getUnsavedState() {
- return mUnsavedState;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (savedInstanceState != null) {
- mSavedState = savedInstanceState.getString(STATE_KEY);
- }
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putString(STATE_KEY, mSavedState);
- }
- }
-
- /**
- * This tests a deliberately odd use of a child fragment, added in onCreateView instead
- * of elsewhere. It simulates creating a UI child fragment added to the view hierarchy
- * created by this fragment.
- */
- public static class ChildFragmentManagerFragment extends StrictFragment {
- private FragmentManager mSavedChildFragmentManager;
- private ChildFragmentManagerChildFragment mChildFragment;
-
- @Override
- public void onAttach(Context context) {
- super.onAttach(context);
- mSavedChildFragmentManager = getChildFragmentManager();
- }
-
- @Nullable
- @Override
- public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- assertSame("child FragmentManagers not the same instance", mSavedChildFragmentManager,
- getChildFragmentManager());
- ChildFragmentManagerChildFragment child =
- (ChildFragmentManagerChildFragment) mSavedChildFragmentManager
- .findFragmentByTag("tag");
- if (child == null) {
- child = new ChildFragmentManagerChildFragment("foo");
- mSavedChildFragmentManager.beginTransaction()
- .add(child, "tag")
- .commitNow();
- assertEquals("argument strings don't match", "foo", child.getString());
- }
- mChildFragment = child;
- return new TextView(container.getContext());
- }
-
- @Nullable
- public Fragment getChildFragment() {
- return mChildFragment;
- }
- }
-
- public static class ChildFragmentManagerChildFragment extends StrictFragment {
- private String mString;
-
- public ChildFragmentManagerChildFragment() {
- }
-
- public ChildFragmentManagerChildFragment(String arg) {
- final Bundle b = new Bundle();
- b.putString("string", arg);
- setArguments(b);
- }
-
- @Override
- public void onAttach(Context context) {
- super.onAttach(context);
- mString = requireArguments().getString("string", "NO VALUE");
- }
-
- public String getString() {
- return mString;
- }
- }
-
- public static class SimpleFragment extends Fragment {
- private int mLayoutId;
- private static final String LAYOUT_ID = "layoutId";
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (savedInstanceState != null) {
- mLayoutId = savedInstanceState.getInt(LAYOUT_ID, mLayoutId);
- }
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putInt(LAYOUT_ID, mLayoutId);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- return inflater.inflate(mLayoutId, container, false);
- }
-
- public static SimpleFragment create(int layoutId) {
- SimpleFragment fragment = new SimpleFragment();
- fragment.mLayoutId = layoutId;
- return fragment;
- }
- }
-
- public static class TargetFragment extends Fragment {
- public boolean calledCreate;
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- calledCreate = true;
- }
- }
-
- public static class ReferrerFragment extends Fragment {
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- Fragment target = getTargetFragment();
- assertNotNull("target fragment was null during referrer onCreate", target);
-
- if (!(target instanceof TargetFragment)) {
- throw new IllegalStateException("target fragment was not a TargetFragment");
- }
-
- assertTrue("target fragment has not yet been created",
- ((TargetFragment) target).calledCreate);
- }
- }
-
- public static class SaveStateFragment extends Fragment {
- private static final String VALUE_KEY = "SaveStateFragment.mValue";
- private int mValue;
-
- public static SaveStateFragment create(int value) {
- SaveStateFragment saveStateFragment = new SaveStateFragment();
- saveStateFragment.mValue = value;
- return saveStateFragment;
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putInt(VALUE_KEY, mValue);
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (savedInstanceState != null) {
- mValue = savedInstanceState.getInt(VALUE_KEY, mValue);
- }
- }
-
- public int getValue() {
- return mValue;
- }
- }
-
- public static class RemoveHelloInOnResume extends Fragment {
- @Override
- public void onResume() {
- super.onResume();
- Fragment fragment = getFragmentManager().findFragmentByTag("Hello");
- if (fragment != null) {
- getFragmentManager().beginTransaction().remove(fragment).commit();
- }
- }
- }
-
- public static class InvalidateOptionFragment extends Fragment {
- public boolean onPrepareOptionsMenuCalled;
-
- public InvalidateOptionFragment() {
- setHasOptionsMenu(true);
- }
-
- @Override
- public void onPrepareOptionsMenu(Menu menu) {
- onPrepareOptionsMenuCalled = true;
- assertNotNull(getContext());
- super.onPrepareOptionsMenu(menu);
- }
- }
-
- public static class OnCreateFragment extends Fragment {
- public boolean onCreateCalled;
-
- public OnCreateFragment() {
- setRetainInstance(true);
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- onCreateCalled = true;
- }
- }
-
- @ContentView(R.layout.nested_retained_inflated_fragment_parent)
- public static class RetainedInflatedParentFragment extends Fragment {
- }
-
- @ContentView(R.layout.nested_inflated_fragment_child)
- public static class RetainedInflatedChildFragment extends Fragment {
-
- int mOnInflateCount = 0;
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setRetainInstance(true);
- }
-
- @Override
- public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs,
- @Nullable Bundle savedInstanceState) {
- super.onInflate(context, attrs, savedInstanceState);
- mOnInflateCount++;
- }
- }
-}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.kt
new file mode 100644
index 0000000..42870f1
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.kt
@@ -0,0 +1,910 @@
+/*
+ * 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 androidx.fragment.app
+
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.os.Parcelable
+import android.util.AttributeSet
+import android.util.Pair
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.annotation.ContentView
+import androidx.core.view.ViewCompat
+import androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks
+import androidx.fragment.app.FragmentTestUtil.HostCallbacks
+import androidx.fragment.app.FragmentTestUtil.shutdownFragmentController
+import androidx.fragment.app.FragmentTestUtil.startupFragmentController
+import androidx.fragment.app.test.EmptyFragmentTestActivity
+import androidx.fragment.app.test.FragmentTestActivity
+import androidx.fragment.test.R
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.ViewModelStore
+import androidx.test.annotation.UiThreadTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.ActivityTestRule
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Assert.fail
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import java.util.concurrent.TimeUnit
+
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+class FragmentLifecycleTest {
+
+ @get:Rule
+ val activityRule = ActivityTestRule(EmptyFragmentTestActivity::class.java)
+
+ @Test
+ fun basicLifecycle() {
+ val fm = activityRule.activity.supportFragmentManager
+ val strictFragment = StrictFragment()
+
+ // Add fragment; StrictFragment will throw if it detects any violation
+ // in standard lifecycle method ordering or expected preconditions.
+ fm.beginTransaction().add(strictFragment, "EmptyHeadless").commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment is not added").that(strictFragment.isAdded).isTrue()
+ assertWithMessage("fragment is detached").that(strictFragment.isDetached).isFalse()
+ assertWithMessage("fragment is not resumed").that(strictFragment.isResumed).isTrue()
+ val lifecycle = strictFragment.lifecycle
+ assertThat(lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ // Test removal as well; StrictFragment will throw here too.
+ fm.beginTransaction().remove(strictFragment).commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment is added").that(strictFragment.isAdded).isFalse()
+ assertWithMessage("fragment is resumed").that(strictFragment.isResumed).isFalse()
+ assertThat(lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED)
+ // Once removed, a new Lifecycle should be created just in case
+ // the developer reuses the same Fragment
+ assertThat(strictFragment.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED)
+
+ // This one is perhaps counterintuitive; "detached" means specifically detached
+ // but still managed by a FragmentManager. The .remove call above
+ // should not enter this state.
+ assertWithMessage("fragment is detached").that(strictFragment.isDetached).isFalse()
+ }
+
+ @Test
+ fun detachment() {
+ val fm = activityRule.activity.supportFragmentManager
+ val f1 = StrictFragment()
+ val f2 = StrictFragment()
+
+ fm.beginTransaction().add(f1, "1").add(f2, "2").commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 1 is not added").that(f1.isAdded).isTrue()
+ assertWithMessage("fragment 2 is not added").that(f2.isAdded).isTrue()
+
+ // Test detaching fragments using StrictFragment to throw on errors.
+ fm.beginTransaction().detach(f1).detach(f2).commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 1 is not detached").that(f1.isDetached).isTrue()
+ assertWithMessage("fragment 2 is not detached").that(f2.isDetached).isTrue()
+ assertWithMessage("fragment 1 is added").that(f1.isAdded).isFalse()
+ assertWithMessage("fragment 2 is added").that(f2.isAdded).isFalse()
+
+ // Only reattach f1; leave v2 detached.
+ fm.beginTransaction().attach(f1).commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 1 is not added").that(f1.isAdded).isTrue()
+ assertWithMessage("fragment 1 is detached").that(f1.isDetached).isFalse()
+ assertWithMessage("fragment 2 is not detached").that(f2.isDetached).isTrue()
+
+ // Remove both from the FragmentManager.
+ fm.beginTransaction().remove(f1).remove(f2).commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 1 is added").that(f1.isAdded).isFalse()
+ assertWithMessage("fragment 2 is added").that(f2.isAdded).isFalse()
+ assertWithMessage("fragment 1 is detached").that(f1.isDetached).isFalse()
+ assertWithMessage("fragment 2 is detached").that(f2.isDetached).isFalse()
+ }
+
+ @Test
+ fun basicBackStack() {
+ val fm = activityRule.activity.supportFragmentManager
+ val f1 = StrictFragment()
+ val f2 = StrictFragment()
+
+ // Add a fragment normally to set up
+ fm.beginTransaction().add(f1, "1").commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 1 is not added").that(f1.isAdded).isTrue()
+
+ // Remove the first one and add a second. We're not using replace() here since
+ // these fragments are headless and as of this test writing, replace() only works
+ // for fragments with views and a container view id.
+ // Add it to the back stack so we can pop it afterwards.
+ fm.beginTransaction().remove(f1).add(f2, "2").addToBackStack("stack1").commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 1 is added").that(f1.isAdded).isFalse()
+ assertWithMessage("fragment 2 is not added").that(f2.isAdded).isTrue()
+
+ // Test popping the stack
+ fm.popBackStack()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 2 is added").that(f2.isAdded).isFalse()
+ assertWithMessage("fragment 1 is not added").that(f1.isAdded).isTrue()
+ }
+
+ @Test
+ fun attachBackStack() {
+ val fm = activityRule.activity.supportFragmentManager
+ val f1 = StrictFragment()
+ val f2 = StrictFragment()
+
+ // Add a fragment normally to set up
+ fm.beginTransaction().add(f1, "1").commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 1 is not added").that(f1.isAdded).isTrue()
+
+ fm.beginTransaction().detach(f1).add(f2, "2").addToBackStack("stack1").commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 1 is not detached").that(f1.isDetached).isTrue()
+ assertWithMessage("fragment 2 is detached").that(f2.isDetached).isFalse()
+ assertWithMessage("fragment 1 is added").that(f1.isAdded).isFalse()
+ assertWithMessage("fragment 2 is not added").that(f2.isAdded).isTrue()
+ }
+
+ @Test
+ fun viewLifecycle() {
+ // Test basic lifecycle when the fragment creates a view
+
+ val fm = activityRule.activity.supportFragmentManager
+ val f1 = StrictViewFragment()
+
+ fm.beginTransaction().add(android.R.id.content, f1).commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 1 is not added").that(f1.isAdded).isTrue()
+ val view = f1.requireView()
+ assertWithMessage("fragment 1 returned null from getView").that(view).isNotNull()
+ assertWithMessage("fragment 1's view is not attached to a window")
+ .that(ViewCompat.isAttachedToWindow(view)).isTrue()
+
+ fm.beginTransaction().remove(f1).commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 1 is added").that(f1.isAdded).isFalse()
+ assertWithMessage("fragment 1 returned non-null from getView after removal")
+ .that(f1.view).isNull()
+ assertWithMessage("fragment 1's previous view is still attached to a window")
+ .that(ViewCompat.isAttachedToWindow(view)).isFalse()
+ }
+
+ @Test
+ fun viewReplace() {
+ // Replace one view with another, then reverse it with the back stack
+
+ val fm = activityRule.activity.supportFragmentManager
+ val f1 = StrictViewFragment()
+ val f2 = StrictViewFragment()
+
+ fm.beginTransaction().add(android.R.id.content, f1).commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 1 is not added").that(f1.isAdded).isTrue()
+
+ val origView1 = f1.requireView()
+ assertWithMessage("fragment 1 returned null view").that(origView1).isNotNull()
+ assertWithMessage("fragment 1's view not attached")
+ .that(ViewCompat.isAttachedToWindow(origView1)).isTrue()
+
+ fm.beginTransaction().replace(android.R.id.content, f2).addToBackStack("stack1").commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 1 is added").that(f1.isAdded).isFalse()
+ assertWithMessage("fragment 2 is added").that(f2.isAdded).isTrue()
+ assertWithMessage("fragment 1 returned non-null view").that(f1.view).isNull()
+ assertWithMessage("fragment 1's old view still attached")
+ .that(ViewCompat.isAttachedToWindow(origView1)).isFalse()
+ val origView2 = f2.requireView()
+ assertWithMessage("fragment 2 returned null view").that(origView2).isNotNull()
+ assertWithMessage("fragment 2's view not attached")
+ .that(ViewCompat.isAttachedToWindow(origView2)).isTrue()
+
+ fm.popBackStack()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 1 is not added").that(f1.isAdded).isTrue()
+ assertWithMessage("fragment 2 is added").that(f2.isAdded).isFalse()
+ assertWithMessage("fragment 2 returned non-null view").that(f2.view).isNull()
+ assertWithMessage("fragment 2's view still attached")
+ .that(ViewCompat.isAttachedToWindow(origView2)).isFalse()
+ val newView1 = f1.requireView()
+ assertWithMessage("fragment 1 had same view from last attachment")
+ .that(newView1).isNotSameAs(origView1)
+ assertWithMessage("fragment 1's view not attached")
+ .that(ViewCompat.isAttachedToWindow(newView1)).isTrue()
+ }
+
+ /**
+ * This test confirms that as long as a parent fragment has called super.onCreate,
+ * any child fragments added, committed and with transactions executed will be brought
+ * to at least the CREATED state by the time the parent fragment receives onCreateView.
+ * This means the child fragment will have received onAttach/onCreate.
+ */
+ @Test
+ @UiThreadTest
+ fun childFragmentManagerAttach() {
+ val viewModelStore = ViewModelStore()
+ val fc = FragmentController.createController(
+ HostCallbacks(activityRule.activity, viewModelStore)
+ )
+ fc.attachHost(null)
+ fc.dispatchCreate()
+
+ val mockLc = mock(FragmentManager.FragmentLifecycleCallbacks::class.java)
+ val mockRecursiveLc = mock(FragmentManager.FragmentLifecycleCallbacks::class.java)
+
+ val fm = fc.supportFragmentManager
+ fm.registerFragmentLifecycleCallbacks(mockLc, false)
+ fm.registerFragmentLifecycleCallbacks(mockRecursiveLc, true)
+
+ val fragment = ChildFragmentManagerFragment()
+ fm.beginTransaction()
+ .add(android.R.id.content, fragment)
+ .commitNow()
+
+ verify<FragmentManager.FragmentLifecycleCallbacks>(mockLc, times(1))
+ .onFragmentCreated(fm, fragment, null)
+
+ fc.dispatchActivityCreated()
+
+ val childFragment = fragment.childFragment!!
+
+ verify<FragmentLifecycleCallbacks>(mockLc, times(1))
+ .onFragmentActivityCreated(fm, fragment, null)
+ verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1))
+ .onFragmentActivityCreated(fm, fragment, null)
+ verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1))
+ .onFragmentActivityCreated(fm, childFragment, null)
+
+ fc.dispatchStart()
+
+ verify<FragmentLifecycleCallbacks>(mockLc, times(1)).onFragmentStarted(fm, fragment)
+ verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1))
+ .onFragmentStarted(fm, fragment)
+ verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1))
+ .onFragmentStarted(fm, childFragment)
+
+ fc.dispatchResume()
+
+ verify<FragmentLifecycleCallbacks>(mockLc, times(1)).onFragmentResumed(fm, fragment)
+ verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1))
+ .onFragmentResumed(fm, fragment)
+ verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1))
+ .onFragmentResumed(fm, childFragment)
+
+ // Confirm that the parent fragment received onAttachFragment
+ assertWithMessage("parent fragment did not receive onAttachFragment")
+ .that(fragment.calledOnAttachFragment).isTrue()
+
+ fc.dispatchStop()
+
+ verify<FragmentLifecycleCallbacks>(mockLc, times(1)).onFragmentStopped(fm, fragment)
+ verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1))
+ .onFragmentStopped(fm, fragment)
+ verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1))
+ .onFragmentStopped(fm, childFragment)
+
+ viewModelStore.clear()
+ fc.dispatchDestroy()
+
+ verify<FragmentLifecycleCallbacks>(mockLc, times(1)).onFragmentDestroyed(fm, fragment)
+ verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1))
+ .onFragmentDestroyed(fm, fragment)
+ verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1))
+ .onFragmentDestroyed(fm, childFragment)
+ }
+
+ /**
+ * This test checks that FragmentLifecycleCallbacks are invoked when expected.
+ */
+ @Test
+ @UiThreadTest
+ fun fragmentLifecycleCallbacks() {
+ val viewModelStore = ViewModelStore()
+ val fc = FragmentController.createController(
+ HostCallbacks(activityRule.activity, viewModelStore)
+ )
+ fc.attachHost(null)
+ fc.dispatchCreate()
+
+ val fm = fc.supportFragmentManager
+
+ val fragment = ChildFragmentManagerFragment()
+ fm.beginTransaction()
+ .add(android.R.id.content, fragment)
+ .commitNow()
+
+ fc.dispatchActivityCreated()
+
+ fc.dispatchStart()
+ fc.dispatchResume()
+
+ // Confirm that the parent fragment received onAttachFragment
+ assertWithMessage("parent fragment did not receive onAttachFragment")
+ .that(fragment.calledOnAttachFragment).isTrue()
+
+ shutdownFragmentController(fc, viewModelStore)
+ }
+
+ /**
+ * This tests that fragments call onDestroy when the activity finishes.
+ */
+ @Test
+ @UiThreadTest
+ fun fragmentDestroyedOnFinish() {
+ val viewModelStore = ViewModelStore()
+ val fc = startupFragmentController(activityRule.activity, null, viewModelStore)
+ val fm = fc.supportFragmentManager
+
+ val fragmentA = StrictViewFragment(R.layout.fragment_a)
+ val fragmentB = StrictViewFragment(R.layout.fragment_b)
+ fm.beginTransaction()
+ .add(android.R.id.content, fragmentA)
+ .commit()
+ fm.executePendingTransactions()
+ fm.beginTransaction()
+ .replace(android.R.id.content, fragmentB)
+ .addToBackStack(null)
+ .commit()
+ fm.executePendingTransactions()
+ shutdownFragmentController(fc, viewModelStore)
+ assertThat(fragmentB.calledOnDestroy).isTrue()
+ assertThat(fragmentA.calledOnDestroy).isTrue()
+ }
+
+ // Make sure that executing transactions during activity lifecycle events
+ // is properly prevented.
+ @Test
+ fun preventReentrantCalls() {
+ testLifecycleTransitionFailure(StrictFragment.ATTACHED, StrictFragment.CREATED)
+ testLifecycleTransitionFailure(StrictFragment.CREATED, StrictFragment.ACTIVITY_CREATED)
+ testLifecycleTransitionFailure(StrictFragment.ACTIVITY_CREATED, StrictFragment.STARTED)
+ testLifecycleTransitionFailure(StrictFragment.STARTED, StrictFragment.RESUMED)
+
+ testLifecycleTransitionFailure(StrictFragment.RESUMED, StrictFragment.STARTED)
+ testLifecycleTransitionFailure(StrictFragment.STARTED, StrictFragment.CREATED)
+ testLifecycleTransitionFailure(StrictFragment.CREATED, StrictFragment.ATTACHED)
+ testLifecycleTransitionFailure(StrictFragment.ATTACHED, StrictFragment.DETACHED)
+ }
+
+ private fun testLifecycleTransitionFailure(fromState: Int, toState: Int) {
+ activityRule.runOnUiThread(Runnable {
+ val viewModelStore = ViewModelStore()
+ val fc1 = startupFragmentController(activityRule.activity, null, viewModelStore)
+
+ val fm1 = fc1.supportFragmentManager
+
+ val reentrantFragment = ReentrantFragment.create(fromState, toState)
+
+ fm1.beginTransaction().add(reentrantFragment, "reentrant").commit()
+ try {
+ fm1.executePendingTransactions()
+ } catch (e: IllegalStateException) {
+ fail("An exception shouldn't happen when initially adding the fragment")
+ }
+
+ // Now shut down the fragment controller. When fromState > toState, this should
+ // result in an exception
+ val savedState: Parcelable?
+ try {
+ fc1.dispatchPause()
+ savedState = fc1.saveAllState()
+ fc1.dispatchStop()
+ fc1.dispatchDestroy()
+ if (fromState > toState) {
+ fail(
+ "Expected IllegalStateException when moving from " +
+ StrictFragment.stateToString(fromState) + " to " +
+ StrictFragment.stateToString(toState)
+ )
+ }
+ } catch (e: IllegalStateException) {
+ if (fromState < toState) {
+ fail(
+ "Unexpected IllegalStateException when moving from " +
+ StrictFragment.stateToString(fromState) + " to " +
+ StrictFragment.stateToString(toState)
+ )
+ }
+ assertThat(e)
+ .hasMessageThat().contains("FragmentManager is already executing transactions")
+ return@Runnable // test passed!
+ }
+
+ // now restore from saved state. This will be reached when
+ // fromState < toState. We want to catch the fragment while it
+ // is being restored as the fragment controller state is being brought up.
+
+ try {
+ startupFragmentController(activityRule.activity, savedState, viewModelStore)
+ fail(
+ "Expected IllegalStateException when moving from " +
+ StrictFragment.stateToString(fromState) + " to " +
+ StrictFragment.stateToString(toState)
+ )
+ } catch (e: IllegalStateException) {
+ assertThat(e)
+ .hasMessageThat().contains("FragmentManager is already executing transactions")
+ }
+ })
+ }
+
+ @Test
+ @UiThreadTest
+ fun testSetArgumentsLifecycle() {
+ val viewModelStore = ViewModelStore()
+ val fc = startupFragmentController(activityRule.activity, null, viewModelStore)
+ val fm = fc.supportFragmentManager
+
+ val f = StrictFragment()
+ f.arguments = Bundle()
+
+ fm.beginTransaction().add(f, "1").commitNow()
+
+ f.arguments = Bundle()
+
+ fc.dispatchPause()
+ fc.saveAllState()
+
+ try {
+ f.arguments = Bundle()
+ } catch (e: IllegalStateException) {
+ assertThat(e)
+ .hasMessageThat().contains("Fragment already added and state has been saved")
+ }
+
+ fc.dispatchStop()
+
+ try {
+ f.arguments = Bundle()
+ } catch (e: IllegalStateException) {
+ assertThat(e)
+ .hasMessageThat().contains("Fragment already added and state has been saved")
+ }
+
+ viewModelStore.clear()
+ fc.dispatchDestroy()
+
+ // Fully destroyed, so fragments have been removed.
+ f.arguments = Bundle()
+ }
+
+ /**
+ * FragmentActivity should not raise the state of a Fragment while it is being destroyed.
+ */
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN_MR1)
+ @Test
+ fun fragmentActivityFinishEarly() {
+ val intent = Intent(activityRule.activity, FragmentTestActivity::class.java)
+ intent.putExtra("finishEarly", true)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+
+ val activity = InstrumentationRegistry.getInstrumentation()
+ .startActivitySync(intent) as FragmentTestActivity
+
+ assertThat(activity.onDestroyLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+ }
+
+ /**
+ * When a fragment is saved in non-config, it should be restored to the same index.
+ */
+ @Test
+ @UiThreadTest
+ fun restoreNonConfig() {
+ var fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, null)
+ val fm = fc.supportFragmentManager
+
+ val backStackRetainedFragment = StrictFragment()
+ backStackRetainedFragment.retainInstance = true
+ val fragment1 = StrictFragment()
+ fm.beginTransaction()
+ .add(backStackRetainedFragment, "backStack")
+ .add(fragment1, "1")
+ .setPrimaryNavigationFragment(fragment1)
+ .addToBackStack(null)
+ .commit()
+ fm.executePendingTransactions()
+ val fragment2 = StrictFragment()
+ fragment2.retainInstance = true
+ fragment2.setTargetFragment(fragment1, 0)
+ val fragment3 = StrictFragment()
+ fm.beginTransaction()
+ .remove(backStackRetainedFragment)
+ .remove(fragment1)
+ .add(fragment2, "2")
+ .add(fragment3, "3")
+ .addToBackStack(null)
+ .commit()
+ fm.executePendingTransactions()
+
+ val savedState = FragmentTestUtil.destroy(activityRule, fc)
+
+ fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, savedState)
+ var foundFragment2 = false
+ for (fragment in fc.supportFragmentManager.fragments) {
+ if (fragment === fragment2) {
+ foundFragment2 = true
+ assertThat(fragment.getTargetFragment()).isNotNull()
+ assertThat(fragment.getTargetFragment()!!.tag).isEqualTo("1")
+ } else {
+ assertThat(fragment.tag).isNotEqualTo("2")
+ }
+ }
+ assertThat(foundFragment2).isTrue()
+ fc.supportFragmentManager.popBackStackImmediate()
+ val foundBackStackRetainedFragment = fc.supportFragmentManager
+ .findFragmentByTag("backStack")
+ assertWithMessage("Retained Fragment on the back stack was not retained")
+ .that(foundBackStackRetainedFragment).isEqualTo(backStackRetainedFragment)
+ }
+
+ /**
+ * Check that retained fragments in the backstack correctly restored after two "configChanges"
+ */
+ @Test
+ @UiThreadTest
+ fun retainedFragmentInBackstack() {
+ var fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, null)
+ var fm = fc.supportFragmentManager
+
+ val fragment1 = StrictFragment()
+ fm.beginTransaction().add(fragment1, "1").addToBackStack(null).commit()
+ fm.executePendingTransactions()
+
+ val child = StrictFragment()
+ child.retainInstance = true
+ fragment1.childFragmentManager.beginTransaction().add(child, "child").commit()
+ fragment1.childFragmentManager.executePendingTransactions()
+
+ val fragment2 = StrictFragment()
+ fm.beginTransaction().remove(fragment1).add(fragment2, "2").addToBackStack(null).commit()
+ fm.executePendingTransactions()
+
+ var savedState = FragmentTestUtil.destroy(activityRule, fc)
+
+ fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, savedState)
+ savedState = FragmentTestUtil.destroy(activityRule, fc)
+ fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, savedState)
+ fm = fc.supportFragmentManager
+ fm.popBackStackImmediate()
+ val retainedChild = fm.findFragmentByTag("1")!!
+ .childFragmentManager.findFragmentByTag("child")
+ assertThat(retainedChild).isEqualTo(child)
+ }
+
+ /**
+ * When there are no retained instance fragments, the FragmentManagerNonConfig's fragments
+ * should be null
+ */
+ @Test
+ @UiThreadTest
+ fun nullNonConfig() {
+ val fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, null)
+ val fm = fc.supportFragmentManager
+
+ val fragment1 = StrictFragment()
+ fm.beginTransaction().add(fragment1, "1").addToBackStack(null).commit()
+ fm.executePendingTransactions()
+ val savedState = FragmentTestUtil.destroy(activityRule, fc)
+ assertThat(savedState.second).isNull()
+ }
+
+ /**
+ * When the FragmentManager state changes, the pending transactions should execute.
+ */
+ @Test
+ @UiThreadTest
+ fun runTransactionsOnChange() {
+ var fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, null)
+ var fm = fc.supportFragmentManager
+
+ val fragment1 = RemoveHelloInOnResume()
+ val fragment2 = StrictFragment()
+ fm.beginTransaction().add(fragment1, "1").setReorderingAllowed(false).commit()
+ fm.beginTransaction().add(fragment2, "Hello").setReorderingAllowed(false).commit()
+ fm.executePendingTransactions()
+
+ assertThat(fm.fragments.size).isEqualTo(2)
+ assertThat(fm.fragments.contains(fragment1)).isTrue()
+ assertThat(fm.fragments.contains(fragment2)).isTrue()
+
+ val savedState = FragmentTestUtil.destroy(activityRule, fc)
+ fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, savedState)
+ fm = fc.supportFragmentManager
+
+ assertThat(fm.fragments.size).isEqualTo(1)
+ for (fragment in fm.fragments) {
+ assertThat(fragment is RemoveHelloInOnResume).isTrue()
+ }
+ }
+
+ @Test
+ @UiThreadTest
+ fun optionsMenu() {
+ val fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, null)
+ val fm = fc.supportFragmentManager
+
+ val fragment = InvalidateOptionFragment()
+ fm.beginTransaction().add(android.R.id.content, fragment).commit()
+ fm.executePendingTransactions()
+
+ val menu = mock(Menu::class.java)
+ fc.dispatchPrepareOptionsMenu(menu)
+ assertThat(fragment.onPrepareOptionsMenuCalled).isTrue()
+ fragment.onPrepareOptionsMenuCalled = false
+ FragmentTestUtil.destroy(activityRule, fc)
+ fc.dispatchPrepareOptionsMenu(menu)
+ assertThat(fragment.onPrepareOptionsMenuCalled).isFalse()
+ }
+
+ /**
+ * When a retained instance fragment is saved while in the back stack, it should go
+ * through onCreate() when it is popped back.
+ */
+ @Test
+ @UiThreadTest
+ fun retainInstanceWithOnCreate() {
+ var fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, null)
+ var fm = fc.supportFragmentManager
+
+ val fragment1 = OnCreateFragment()
+
+ fm.beginTransaction().add(fragment1, "1").commit()
+ fm.beginTransaction().remove(fragment1).addToBackStack(null).commit()
+
+ var savedState = FragmentTestUtil.destroy(activityRule, fc)
+ val restartState = Pair.create<Parcelable, FragmentManagerNonConfig>(savedState.first, null)
+
+ fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, restartState)
+
+ // Save again, but keep the state
+ savedState = FragmentTestUtil.destroy(activityRule, fc)
+
+ fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, savedState)
+
+ fm = fc.supportFragmentManager
+
+ fm.popBackStackImmediate()
+ val fragment2 = fm.findFragmentByTag("1") as OnCreateFragment
+ assertThat(fragment2.onCreateCalled).isTrue()
+ fm.popBackStackImmediate()
+ }
+
+ /**
+ * A retained instance fragment should go through onCreate() once, even through save and
+ * restore.
+ */
+ @Test
+ @UiThreadTest
+ fun retainInstanceOneOnCreate() {
+ var fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, null)
+ var fm = fc.supportFragmentManager
+
+ val fragment = OnCreateFragment()
+
+ fm.beginTransaction().add(fragment, "fragment").commit()
+ fm.executePendingTransactions()
+
+ fm.beginTransaction().remove(fragment).addToBackStack(null).commit()
+
+ assertThat(fragment.onCreateCalled).isTrue()
+ fragment.onCreateCalled = false
+
+ val savedState = FragmentTestUtil.destroy(activityRule, fc)
+
+ fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, savedState)
+ fm = fc.supportFragmentManager
+
+ fm.popBackStackImmediate()
+ assertThat(fragment.onCreateCalled).isFalse()
+ }
+
+ /**
+ * A retained instance fragment added via XML should go through onCreate() once, but should get
+ * onInflate calls for each inflation.
+ */
+ @Test
+ @UiThreadTest
+ fun retainInstanceLayoutOnInflate() {
+ var fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, null)
+ var fm = fc.supportFragmentManager
+
+ var parentFragment = RetainedInflatedParentFragment()
+
+ fm.beginTransaction().add(android.R.id.content, parentFragment).commit()
+ fm.executePendingTransactions()
+
+ val childFragment = parentFragment.childFragmentManager
+ .findFragmentById(R.id.child_fragment) as RetainedInflatedChildFragment
+
+ fm.beginTransaction().remove(parentFragment).addToBackStack(null).commit()
+
+ val savedState = FragmentTestUtil.destroy(activityRule, fc)
+
+ fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, savedState)
+ fm = fc.supportFragmentManager
+
+ fm.popBackStackImmediate()
+
+ parentFragment = fm.findFragmentById(android.R.id.content) as RetainedInflatedParentFragment
+ val childFragment2 = parentFragment.childFragmentManager
+ .findFragmentById(R.id.child_fragment) as RetainedInflatedChildFragment
+
+ assertWithMessage("Child Fragment should be retained")
+ .that(childFragment2).isEqualTo(childFragment)
+ assertWithMessage("Child Fragment should have onInflate called twice")
+ .that(childFragment2.mOnInflateCount).isEqualTo(2)
+ }
+
+ private fun executePendingTransactions(fm: FragmentManager) {
+ activityRule.runOnUiThread { fm.executePendingTransactions() }
+ }
+
+ /**
+ * This tests a deliberately odd use of a child fragment, added in onCreateView instead
+ * of elsewhere. It simulates creating a UI child fragment added to the view hierarchy
+ * created by this fragment.
+ */
+ class ChildFragmentManagerFragment : StrictFragment() {
+ private lateinit var savedChildFragmentManager: FragmentManager
+ private lateinit var childFragmentManagerChildFragment: ChildFragmentManagerChildFragment
+
+ val childFragment: Fragment?
+ get() = childFragmentManagerChildFragment
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ savedChildFragmentManager = childFragmentManager
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ) = TextView(inflater.context).also {
+ assertWithMessage("child FragmentManagers not the same instance")
+ .that(childFragmentManager).isSameAs(savedChildFragmentManager)
+ var child = savedChildFragmentManager
+ .findFragmentByTag("tag") as ChildFragmentManagerChildFragment?
+ if (child == null) {
+ child = ChildFragmentManagerChildFragment("foo")
+ savedChildFragmentManager.beginTransaction().add(child, "tag").commitNow()
+ assertWithMessage("argument strings don't match")
+ .that(child.string).isEqualTo("foo")
+ }
+ childFragmentManagerChildFragment = child
+ }
+ }
+
+ class ChildFragmentManagerChildFragment : StrictFragment {
+ lateinit var string: String
+ private set
+
+ constructor()
+
+ constructor(arg: String) {
+ val b = Bundle()
+ b.putString("string", arg)
+ arguments = b
+ }
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ string = requireArguments().getString("string", "NO VALUE")
+ }
+ }
+
+ class RemoveHelloInOnResume : Fragment() {
+ override fun onResume() {
+ super.onResume()
+ val fragment = fragmentManager!!.findFragmentByTag("Hello")
+ if (fragment != null) {
+ fragmentManager!!.beginTransaction().remove(fragment).commit()
+ }
+ }
+ }
+
+ class InvalidateOptionFragment : Fragment() {
+ var onPrepareOptionsMenuCalled: Boolean = false
+
+ init {
+ setHasOptionsMenu(true)
+ }
+
+ override fun onPrepareOptionsMenu(menu: Menu) {
+ onPrepareOptionsMenuCalled = true
+ assertThat(context).isNotNull()
+ super.onPrepareOptionsMenu(menu)
+ }
+ }
+
+ class OnCreateFragment : Fragment() {
+ var onCreateCalled: Boolean = false
+
+ init {
+ retainInstance = true
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ onCreateCalled = true
+ }
+ }
+
+ @ContentView(R.layout.nested_retained_inflated_fragment_parent)
+ class RetainedInflatedParentFragment : Fragment()
+
+ @ContentView(R.layout.nested_inflated_fragment_child)
+ class RetainedInflatedChildFragment : Fragment() {
+ internal var mOnInflateCount = 0
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ retainInstance = true
+ }
+
+ override fun onInflate(context: Context, attrs: AttributeSet, savedInstanceState: Bundle?) {
+ super.onInflate(context, attrs, savedInstanceState)
+ mOnInflateCount++
+ }
+ }
+}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/FragmentReorderingTest.java b/fragment/src/androidTest/java/androidx/fragment/app/FragmentReorderingTest.java
deleted file mode 100644
index 1c47bc8..0000000
--- a/fragment/src/androidTest/java/androidx/fragment/app/FragmentReorderingTest.java
+++ /dev/null
@@ -1,683 +0,0 @@
-/*
- * 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 androidx.fragment.app;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import android.app.Instrumentation;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.EditText;
-
-import androidx.fragment.app.test.FragmentTestActivity;
-import androidx.fragment.test.R;
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.rule.ActivityTestRule;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class FragmentReorderingTest {
- @Rule
- public ActivityTestRule<FragmentTestActivity> mActivityRule =
- new ActivityTestRule<FragmentTestActivity>(FragmentTestActivity.class);
-
- private ViewGroup mContainer;
- private FragmentManager mFM;
- private Instrumentation mInstrumentation;
-
- @Before
- public void setup() {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- mContainer = (ViewGroup) mActivityRule.getActivity().findViewById(R.id.fragmentContainer);
- mFM = mActivityRule.getActivity().getSupportFragmentManager();
- mInstrumentation = InstrumentationRegistry.getInstrumentation();
- }
-
- // Test that when you add and replace a fragment that only the replace's add
- // actually creates a View.
- @Test
- public void addReplace() throws Throwable {
- final CountCallsFragment fragment1 = new CountCallsFragment();
- final StrictViewFragment fragment2 = new StrictViewFragment();
- mInstrumentation.runOnMainSync(new Runnable() {
- @Override
- public void run() {
- mFM.beginTransaction()
- .add(R.id.fragmentContainer, fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.beginTransaction()
- .replace(R.id.fragmentContainer, fragment2)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.executePendingTransactions();
- }
- });
- assertEquals(0, fragment1.onCreateViewCount);
- FragmentTestUtil.assertChildren(mContainer, fragment2);
-
- mInstrumentation.runOnMainSync(new Runnable() {
- @Override
- public void run() {
- mFM.popBackStack();
- mFM.popBackStack();
- mFM.executePendingTransactions();
- }
- });
- FragmentTestUtil.assertChildren(mContainer);
- }
-
- // Test that it is possible to merge a transaction that starts with pop and adds
- // the same view back again.
- @Test
- public void startWithPop() throws Throwable {
- // Start with a single fragment on the back stack
- final CountCallsFragment fragment1 = new CountCallsFragment();
- mFM.beginTransaction()
- .add(R.id.fragmentContainer, fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
-
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- FragmentTestUtil.assertChildren(mContainer, fragment1);
-
- // Now pop and add
- mInstrumentation.runOnMainSync(new Runnable() {
- @Override
- public void run() {
- mFM.popBackStack();
- mFM.beginTransaction()
- .add(R.id.fragmentContainer, fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.executePendingTransactions();
- }
- });
- FragmentTestUtil.assertChildren(mContainer, fragment1);
- assertEquals(1, fragment1.onCreateViewCount);
-
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
- FragmentTestUtil.assertChildren(mContainer);
- assertEquals(1, fragment1.onCreateViewCount);
- }
-
- // Popping the back stack in the middle of other operations doesn't fool it.
- @Test
- public void middlePop() throws Throwable {
- final CountCallsFragment fragment1 = new CountCallsFragment();
- final CountCallsFragment fragment2 = new CountCallsFragment();
- mInstrumentation.runOnMainSync(new Runnable() {
- @Override
- public void run() {
- mFM.beginTransaction()
- .add(R.id.fragmentContainer, fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.popBackStack();
- mFM.beginTransaction()
- .add(R.id.fragmentContainer, fragment2)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.executePendingTransactions();
- }
- });
- FragmentTestUtil.assertChildren(mContainer, fragment2);
- assertEquals(0, fragment1.onAttachCount);
- assertEquals(1, fragment2.onCreateViewCount);
-
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
- FragmentTestUtil.assertChildren(mContainer);
- assertEquals(1, fragment2.onDetachCount);
- }
-
- // ensure that removing a view after adding it is optimized into no
- // View being created. Hide still gets notified.
- @Test
- public void removeRedundantRemove() throws Throwable {
- final CountCallsFragment fragment1 = new CountCallsFragment();
- final int[] id = new int[1];
- mInstrumentation.runOnMainSync(new Runnable() {
- @Override
- public void run() {
- id[0] = mFM.beginTransaction()
- .add(R.id.fragmentContainer, fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.beginTransaction()
- .hide(fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.beginTransaction()
- .remove(fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.executePendingTransactions();
- }
- });
- FragmentTestUtil.assertChildren(mContainer);
- assertEquals(0, fragment1.onCreateViewCount);
- assertEquals(1, fragment1.onHideCount);
- assertEquals(0, fragment1.onShowCount);
- assertEquals(0, fragment1.onDetachCount);
- assertEquals(1, fragment1.onAttachCount);
-
- FragmentTestUtil.popBackStackImmediate(mActivityRule, id[0],
- FragmentManager.POP_BACK_STACK_INCLUSIVE);
- FragmentTestUtil.assertChildren(mContainer);
- assertEquals(0, fragment1.onCreateViewCount);
- assertEquals(1, fragment1.onHideCount);
- assertEquals(1, fragment1.onShowCount);
- assertEquals(1, fragment1.onDetachCount);
- assertEquals(1, fragment1.onAttachCount);
- }
-
- // Ensure that removing and adding the same view results in no operation
- @Test
- public void removeRedundantAdd() throws Throwable {
- final CountCallsFragment fragment1 = new CountCallsFragment();
- int id = mFM.beginTransaction()
- .add(R.id.fragmentContainer, fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- assertEquals(1, fragment1.onCreateViewCount);
-
- mInstrumentation.runOnMainSync(new Runnable() {
- @Override
- public void run() {
- mFM.beginTransaction()
- .remove(fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.beginTransaction()
- .add(R.id.fragmentContainer, fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.executePendingTransactions();
- }
- });
-
- FragmentTestUtil.assertChildren(mContainer, fragment1);
- // should be optimized out
- assertEquals(1, fragment1.onCreateViewCount);
-
- mFM.popBackStack(id, 0);
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- FragmentTestUtil.assertChildren(mContainer, fragment1);
- // optimize out going back, too
- assertEquals(1, fragment1.onCreateViewCount);
- }
-
- // detaching, then attaching results in on change. Hide still functions
- @Test
- public void removeRedundantAttach() throws Throwable {
- final CountCallsFragment fragment1 = new CountCallsFragment();
- int id = mFM.beginTransaction()
- .add(R.id.fragmentContainer, fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- assertEquals(1, fragment1.onAttachCount);
- FragmentTestUtil.assertChildren(mContainer, fragment1);
-
- mInstrumentation.runOnMainSync(new Runnable() {
- @Override
- public void run() {
- mFM.beginTransaction()
- .detach(fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.beginTransaction()
- .hide(fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.beginTransaction()
- .attach(fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.executePendingTransactions();
- }
- });
-
- FragmentTestUtil.assertChildren(mContainer, fragment1);
- // can optimize out the detach/attach
- assertEquals(0, fragment1.onDestroyViewCount);
- assertEquals(1, fragment1.onHideCount);
- assertEquals(0, fragment1.onShowCount);
- assertEquals(1, fragment1.onCreateViewCount);
- assertEquals(1, fragment1.onAttachCount);
- assertEquals(0, fragment1.onDetachCount);
-
- mFM.popBackStack(id, 0);
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- FragmentTestUtil.assertChildren(mContainer, fragment1);
-
- // optimized out again, but not the show
- assertEquals(0, fragment1.onDestroyViewCount);
- assertEquals(1, fragment1.onHideCount);
- assertEquals(1, fragment1.onShowCount);
- assertEquals(1, fragment1.onCreateViewCount);
- assertEquals(1, fragment1.onAttachCount);
- assertEquals(0, fragment1.onDetachCount);
- }
-
- // attaching, then detaching shouldn't result in a View being created
- @Test
- public void removeRedundantDetach() throws Throwable {
- final CountCallsFragment fragment1 = new CountCallsFragment();
- int id = mFM.beginTransaction()
- .add(R.id.fragmentContainer, fragment1)
- .detach(fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- // the add detach is not fully optimized out
- assertEquals(1, fragment1.onAttachCount);
- assertEquals(0, fragment1.onDetachCount);
- assertTrue(fragment1.isDetached());
- assertEquals(0, fragment1.onCreateViewCount);
- FragmentTestUtil.assertChildren(mContainer);
-
- mInstrumentation.runOnMainSync(new Runnable() {
- @Override
- public void run() {
- mFM.beginTransaction()
- .attach(fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.beginTransaction()
- .hide(fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.beginTransaction()
- .detach(fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.executePendingTransactions();
- }
- });
-
- FragmentTestUtil.assertChildren(mContainer);
- // can optimize out the attach/detach, and the hide call
- assertEquals(1, fragment1.onAttachCount);
- assertEquals(0, fragment1.onDetachCount);
- assertEquals(1, fragment1.onHideCount);
- assertTrue(fragment1.isHidden());
- assertEquals(0, fragment1.onShowCount);
-
- mFM.popBackStack(id, 0);
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- FragmentTestUtil.assertChildren(mContainer);
-
- // we can optimize out the attach/detach on the way back
- assertEquals(1, fragment1.onAttachCount);
- assertEquals(0, fragment1.onDetachCount);
- assertEquals(1, fragment1.onShowCount);
- assertEquals(1, fragment1.onHideCount);
- assertFalse(fragment1.isHidden());
- }
-
- // show, then hide should optimize out
- @Test
- public void removeRedundantHide() throws Throwable {
- final CountCallsFragment fragment1 = new CountCallsFragment();
- int id = mFM.beginTransaction()
- .add(R.id.fragmentContainer, fragment1)
- .hide(fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- assertEquals(0, fragment1.onShowCount);
- assertEquals(1, fragment1.onHideCount);
- FragmentTestUtil.assertChildren(mContainer, fragment1);
-
- mInstrumentation.runOnMainSync(new Runnable() {
- @Override
- public void run() {
- mFM.beginTransaction()
- .show(fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.beginTransaction()
- .remove(fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.beginTransaction()
- .add(R.id.fragmentContainer, fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.beginTransaction()
- .hide(fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.executePendingTransactions();
- }
- });
-
- FragmentTestUtil.assertChildren(mContainer, fragment1);
- // optimize out hide/show
- assertEquals(0, fragment1.onShowCount);
- assertEquals(1, fragment1.onHideCount);
-
- FragmentTestUtil.popBackStackImmediate(mActivityRule, id, 0);
- FragmentTestUtil.assertChildren(mContainer, fragment1);
-
- // still optimized out
- assertEquals(0, fragment1.onShowCount);
- assertEquals(1, fragment1.onHideCount);
-
- mInstrumentation.runOnMainSync(new Runnable() {
- @Override
- public void run() {
- mFM.beginTransaction()
- .show(fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.beginTransaction()
- .hide(fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.executePendingTransactions();
- }
- });
-
- // The show/hide can be optimized out and nothing should change.
- FragmentTestUtil.assertChildren(mContainer, fragment1);
- assertEquals(0, fragment1.onShowCount);
- assertEquals(1, fragment1.onHideCount);
-
- FragmentTestUtil.popBackStackImmediate(mActivityRule, id, 0);
- FragmentTestUtil.assertChildren(mContainer, fragment1);
-
- assertEquals(0, fragment1.onShowCount);
- assertEquals(1, fragment1.onHideCount);
-
- mInstrumentation.runOnMainSync(new Runnable() {
- @Override
- public void run() {
- mFM.beginTransaction()
- .show(fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.beginTransaction()
- .detach(fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.beginTransaction()
- .attach(fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.beginTransaction()
- .hide(fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.executePendingTransactions();
- }
- });
-
- // the detach/attach should not affect the show/hide, so show/hide should cancel each other
- assertEquals(0, fragment1.onShowCount);
- assertEquals(1, fragment1.onHideCount);
-
- FragmentTestUtil.popBackStackImmediate(mActivityRule, id, 0);
- FragmentTestUtil.assertChildren(mContainer, fragment1);
-
- assertEquals(0, fragment1.onShowCount);
- assertEquals(1, fragment1.onHideCount);
- }
-
- // hiding and showing the same view should optimize out
- @Test
- public void removeRedundantShow() throws Throwable {
- final CountCallsFragment fragment1 = new CountCallsFragment();
- int id = mFM.beginTransaction()
- .add(R.id.fragmentContainer, fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- assertEquals(0, fragment1.onShowCount);
- assertEquals(0, fragment1.onHideCount);
- FragmentTestUtil.assertChildren(mContainer, fragment1);
-
- mInstrumentation.runOnMainSync(new Runnable() {
- @Override
- public void run() {
- mFM.beginTransaction()
- .hide(fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.beginTransaction()
- .detach(fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.beginTransaction()
- .attach(fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.beginTransaction()
- .show(fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.executePendingTransactions();
- }
- });
-
- FragmentTestUtil.assertChildren(mContainer, fragment1);
- // can optimize out the show/hide
- assertEquals(0, fragment1.onShowCount);
- assertEquals(0, fragment1.onHideCount);
-
- FragmentTestUtil.popBackStackImmediate(mActivityRule, id,
- FragmentManager.POP_BACK_STACK_INCLUSIVE);
- assertEquals(0, fragment1.onShowCount);
- assertEquals(0, fragment1.onHideCount);
- }
-
- // The View order shouldn't be messed up by reordering -- a view that
- // is optimized to not remove/add should be in its correct position after
- // the transaction completes.
- @Test
- public void viewOrder() throws Throwable {
- final CountCallsFragment fragment1 = new CountCallsFragment();
- int id = mFM.beginTransaction()
- .add(R.id.fragmentContainer, fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- FragmentTestUtil.assertChildren(mContainer, fragment1);
-
- final CountCallsFragment fragment2 = new CountCallsFragment();
-
- mInstrumentation.runOnMainSync(new Runnable() {
- @Override
- public void run() {
- mFM.beginTransaction()
- .replace(R.id.fragmentContainer, fragment2)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.beginTransaction()
- .add(R.id.fragmentContainer, fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
-
- mFM.executePendingTransactions();
- }
- });
- FragmentTestUtil.assertChildren(mContainer, fragment2, fragment1);
-
- FragmentTestUtil.popBackStackImmediate(mActivityRule, id, 0);
- FragmentTestUtil.assertChildren(mContainer, fragment1);
- }
-
- // Popping an added transaction results in no operation
- @Test
- public void addPopBackStack() throws Throwable {
- final CountCallsFragment fragment1 = new CountCallsFragment();
- mInstrumentation.runOnMainSync(new Runnable() {
- @Override
- public void run() {
- mFM.beginTransaction()
- .add(R.id.fragmentContainer, fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.popBackStack();
- mFM.executePendingTransactions();
- }
- });
- FragmentTestUtil.assertChildren(mContainer);
-
- // Was never instantiated because it was popped before anything could happen
- assertEquals(0, fragment1.onCreateViewCount);
- }
-
- // A non-back-stack transaction doesn't interfere with back stack add/pop
- // optimization.
- @Test
- public void popNonBackStack() throws Throwable {
- final CountCallsFragment fragment1 = new CountCallsFragment();
- final CountCallsFragment fragment2 = new CountCallsFragment();
- mInstrumentation.runOnMainSync(new Runnable() {
- @Override
- public void run() {
- mFM.beginTransaction()
- .add(R.id.fragmentContainer, fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.beginTransaction()
- .replace(R.id.fragmentContainer, fragment2)
- .setReorderingAllowed(true)
- .commit();
- mFM.popBackStack();
- mFM.executePendingTransactions();
- }
- });
- FragmentTestUtil.assertChildren(mContainer, fragment2);
-
- // It should be optimized with the replace, so no View creation
- assertEquals(0, fragment1.onCreateViewCount);
- }
-
- // When reordering is disabled, the transaction prior to the disabled reordering
- // transaction should all be run prior to running the ordered transaction.
- @Test
- public void noReordering() throws Throwable {
- final CountCallsFragment fragment1 = new CountCallsFragment();
- final CountCallsFragment fragment2 = new CountCallsFragment();
- mInstrumentation.runOnMainSync(new Runnable() {
- @Override
- public void run() {
- mFM.beginTransaction()
- .add(R.id.fragmentContainer, fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.beginTransaction()
- .replace(R.id.fragmentContainer, fragment2)
- .addToBackStack(null)
- .setReorderingAllowed(false)
- .commit();
- mFM.executePendingTransactions();
- }
- });
- FragmentTestUtil.assertChildren(mContainer, fragment2);
-
- // No reordering, so fragment1 should have created its View
- assertEquals(1, fragment1.onCreateViewCount);
- }
-
- // Test that a fragment view that is created with focus has focus after the transaction
- // completes.
- @UiThreadTest
- @Test
- public void focusedView() throws Throwable {
- mActivityRule.getActivity().setContentView(R.layout.double_container);
- mContainer = (ViewGroup) mActivityRule.getActivity().findViewById(R.id.fragmentContainer1);
- EditText firstEditText = new EditText(mContainer.getContext());
- mContainer.addView(firstEditText);
- firstEditText.requestFocus();
-
- assertTrue(firstEditText.isFocused());
- final CountCallsFragment fragment1 = new CountCallsFragment();
- final CountCallsFragment fragment2 = new CountCallsFragment();
- fragment2.setLayoutId(R.layout.with_edit_text);
- mFM.beginTransaction()
- .add(R.id.fragmentContainer2, fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.beginTransaction()
- .replace(R.id.fragmentContainer2, fragment2)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- mFM.executePendingTransactions();
- final View editText = fragment2.requireView().findViewById(R.id.editText);
- assertTrue(editText.isFocused());
- assertFalse(firstEditText.isFocused());
- }
-}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/FragmentReorderingTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/FragmentReorderingTest.kt
new file mode 100644
index 0000000..6877430
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/FragmentReorderingTest.kt
@@ -0,0 +1,633 @@
+/*
+ * 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 androidx.fragment.app
+
+import android.app.Instrumentation
+import android.view.View
+import android.view.ViewGroup
+import android.widget.EditText
+import androidx.fragment.app.test.FragmentTestActivity
+import androidx.fragment.test.R
+import androidx.test.annotation.UiThreadTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.ActivityTestRule
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FragmentReorderingTest {
+ @get:Rule
+ var activityRule = ActivityTestRule(FragmentTestActivity::class.java)
+
+ private lateinit var container: ViewGroup
+ private lateinit var fm: FragmentManager
+ private lateinit var instrumentation: Instrumentation
+
+ @Before
+ fun setup() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ container = activityRule.activity.findViewById<View>(R.id.fragmentContainer) as ViewGroup
+ fm = activityRule.activity.supportFragmentManager
+ instrumentation = InstrumentationRegistry.getInstrumentation()
+ }
+
+ // Test that when you add and replace a fragment that only the replace's add
+ // actually creates a View.
+ @Test
+ fun addReplace() {
+ val fragment1 = CountCallsFragment()
+ val fragment2 = StrictViewFragment()
+ instrumentation.runOnMainSync {
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.beginTransaction()
+ .replace(R.id.fragmentContainer, fragment2)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.executePendingTransactions()
+ }
+ assertThat(fragment1.onCreateViewCount).isEqualTo(0)
+ FragmentTestUtil.assertChildren(container, fragment2)
+
+ instrumentation.runOnMainSync {
+ fm.popBackStack()
+ fm.popBackStack()
+ fm.executePendingTransactions()
+ }
+ FragmentTestUtil.assertChildren(container)
+ }
+
+ // Test that it is possible to merge a transaction that starts with pop and adds
+ // the same view back again.
+ @Test
+ fun startWithPop() {
+ // Start with a single fragment on the back stack
+ val fragment1 = CountCallsFragment()
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ FragmentTestUtil.assertChildren(container, fragment1)
+
+ // Now pop and add
+ instrumentation.runOnMainSync {
+ fm.popBackStack()
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.executePendingTransactions()
+ }
+ FragmentTestUtil.assertChildren(container, fragment1)
+ assertThat(fragment1.onCreateViewCount).isEqualTo(1)
+
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+ FragmentTestUtil.assertChildren(container)
+ assertThat(fragment1.onCreateViewCount).isEqualTo(1)
+ }
+
+ // Popping the back stack in the middle of other operations doesn't fool it.
+ @Test
+ fun middlePop() {
+ val fragment1 = CountCallsFragment()
+ val fragment2 = CountCallsFragment()
+ instrumentation.runOnMainSync {
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.popBackStack()
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment2)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.executePendingTransactions()
+ }
+ FragmentTestUtil.assertChildren(container, fragment2)
+ assertThat(fragment1.onAttachCount).isEqualTo(0)
+ assertThat(fragment2.onCreateViewCount).isEqualTo(1)
+
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+ FragmentTestUtil.assertChildren(container)
+ assertThat(fragment2.onDetachCount).isEqualTo(1)
+ }
+
+ // ensure that removing a view after adding it is optimized into no
+ // View being created. Hide still gets notified.
+ @Test
+ fun removeRedundantRemove() {
+ val fragment1 = CountCallsFragment()
+ var id = -1
+ instrumentation.runOnMainSync {
+ id = fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.beginTransaction()
+ .hide(fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.beginTransaction()
+ .remove(fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.executePendingTransactions()
+ }
+ FragmentTestUtil.assertChildren(container)
+ assertThat(fragment1.onCreateViewCount).isEqualTo(0)
+ assertThat(fragment1.onHideCount).isEqualTo(1)
+ assertThat(fragment1.onShowCount).isEqualTo(0)
+ assertThat(fragment1.onDetachCount).isEqualTo(0)
+ assertThat(fragment1.onAttachCount).isEqualTo(1)
+
+ FragmentTestUtil.popBackStackImmediate(
+ activityRule,
+ id,
+ FragmentManager.POP_BACK_STACK_INCLUSIVE
+ )
+ FragmentTestUtil.assertChildren(container)
+ assertThat(fragment1.onCreateViewCount).isEqualTo(0)
+ assertThat(fragment1.onHideCount).isEqualTo(1)
+ assertThat(fragment1.onShowCount).isEqualTo(1)
+ assertThat(fragment1.onDetachCount).isEqualTo(1)
+ assertThat(fragment1.onAttachCount).isEqualTo(1)
+ }
+
+ // Ensure that removing and adding the same view results in no operation
+ @Test
+ fun removeRedundantAdd() {
+ val fragment1 = CountCallsFragment()
+ val id = fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ assertThat(fragment1.onCreateViewCount).isEqualTo(1)
+
+ instrumentation.runOnMainSync {
+ fm.beginTransaction()
+ .remove(fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.executePendingTransactions()
+ }
+
+ FragmentTestUtil.assertChildren(container, fragment1)
+ // should be optimized out
+ assertThat(fragment1.onCreateViewCount).isEqualTo(1)
+
+ fm.popBackStack(id, 0)
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ FragmentTestUtil.assertChildren(container, fragment1)
+ // optimize out going back, too
+ assertThat(fragment1.onCreateViewCount).isEqualTo(1)
+ }
+
+ // detaching, then attaching results in on change. Hide still functions
+ @Test
+ fun removeRedundantAttach() {
+ val fragment1 = CountCallsFragment()
+ val id = fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ assertThat(fragment1.onAttachCount).isEqualTo(1)
+ FragmentTestUtil.assertChildren(container, fragment1)
+
+ instrumentation.runOnMainSync {
+ fm.beginTransaction()
+ .detach(fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.beginTransaction()
+ .hide(fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.beginTransaction()
+ .attach(fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.executePendingTransactions()
+ }
+
+ FragmentTestUtil.assertChildren(container, fragment1)
+ // can optimize out the detach/attach
+ assertThat(fragment1.onDestroyViewCount).isEqualTo(0)
+ assertThat(fragment1.onHideCount).isEqualTo(1)
+ assertThat(fragment1.onShowCount).isEqualTo(0)
+ assertThat(fragment1.onCreateViewCount).isEqualTo(1)
+ assertThat(fragment1.onAttachCount).isEqualTo(1)
+ assertThat(fragment1.onDetachCount).isEqualTo(0)
+
+ fm.popBackStack(id, 0)
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ FragmentTestUtil.assertChildren(container, fragment1)
+
+ // optimized out again, but not the show
+ assertThat(fragment1.onDestroyViewCount).isEqualTo(0)
+ assertThat(fragment1.onHideCount).isEqualTo(1)
+ assertThat(fragment1.onShowCount).isEqualTo(1)
+ assertThat(fragment1.onCreateViewCount).isEqualTo(1)
+ assertThat(fragment1.onAttachCount).isEqualTo(1)
+ assertThat(fragment1.onDetachCount).isEqualTo(0)
+ }
+
+ // attaching, then detaching shouldn't result in a View being created
+ @Test
+ fun removeRedundantDetach() {
+ val fragment1 = CountCallsFragment()
+ val id = fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment1)
+ .detach(fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ // the add detach is not fully optimized out
+ assertThat(fragment1.onAttachCount).isEqualTo(1)
+ assertThat(fragment1.onDetachCount).isEqualTo(0)
+ assertThat(fragment1.isDetached).isTrue()
+ assertThat(fragment1.onCreateViewCount).isEqualTo(0)
+ FragmentTestUtil.assertChildren(container)
+
+ instrumentation.runOnMainSync {
+ fm.beginTransaction()
+ .attach(fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.beginTransaction()
+ .hide(fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.beginTransaction()
+ .detach(fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.executePendingTransactions()
+ }
+
+ FragmentTestUtil.assertChildren(container)
+ // can optimize out the attach/detach, and the hide call
+ assertThat(fragment1.onAttachCount).isEqualTo(1)
+ assertThat(fragment1.onDetachCount).isEqualTo(0)
+ assertThat(fragment1.onHideCount).isEqualTo(1)
+ assertThat(fragment1.isHidden).isTrue()
+ assertThat(fragment1.onShowCount).isEqualTo(0)
+
+ fm.popBackStack(id, 0)
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ FragmentTestUtil.assertChildren(container)
+
+ // we can optimize out the attach/detach on the way back
+ assertThat(fragment1.onAttachCount).isEqualTo(1)
+ assertThat(fragment1.onDetachCount).isEqualTo(0)
+ assertThat(fragment1.onShowCount).isEqualTo(1)
+ assertThat(fragment1.onHideCount).isEqualTo(1)
+ assertThat(fragment1.isHidden).isFalse()
+ }
+
+ // show, then hide should optimize out
+ @Test
+ fun removeRedundantHide() {
+ val fragment1 = CountCallsFragment()
+ val id = fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment1)
+ .hide(fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ assertThat(fragment1.onShowCount).isEqualTo(0)
+ assertThat(fragment1.onHideCount).isEqualTo(1)
+ FragmentTestUtil.assertChildren(container, fragment1)
+
+ instrumentation.runOnMainSync {
+ fm.beginTransaction()
+ .show(fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.beginTransaction()
+ .remove(fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.beginTransaction()
+ .hide(fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.executePendingTransactions()
+ }
+
+ FragmentTestUtil.assertChildren(container, fragment1)
+ // optimize out hide/show
+ assertThat(fragment1.onShowCount).isEqualTo(0)
+ assertThat(fragment1.onHideCount).isEqualTo(1)
+
+ FragmentTestUtil.popBackStackImmediate(activityRule, id, 0)
+ FragmentTestUtil.assertChildren(container, fragment1)
+
+ // still optimized out
+ assertThat(fragment1.onShowCount).isEqualTo(0)
+ assertThat(fragment1.onHideCount).isEqualTo(1)
+
+ instrumentation.runOnMainSync {
+ fm.beginTransaction()
+ .show(fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.beginTransaction()
+ .hide(fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.executePendingTransactions()
+ }
+
+ // The show/hide can be optimized out and nothing should change.
+ FragmentTestUtil.assertChildren(container, fragment1)
+ assertThat(fragment1.onShowCount).isEqualTo(0)
+ assertThat(fragment1.onHideCount).isEqualTo(1)
+
+ FragmentTestUtil.popBackStackImmediate(activityRule, id, 0)
+ FragmentTestUtil.assertChildren(container, fragment1)
+
+ assertThat(fragment1.onShowCount).isEqualTo(0)
+ assertThat(fragment1.onHideCount).isEqualTo(1)
+
+ instrumentation.runOnMainSync {
+ fm.beginTransaction()
+ .show(fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.beginTransaction()
+ .detach(fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.beginTransaction()
+ .attach(fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.beginTransaction()
+ .hide(fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.executePendingTransactions()
+ }
+
+ // the detach/attach should not affect the show/hide, so show/hide should cancel each other
+ assertThat(fragment1.onShowCount).isEqualTo(0)
+ assertThat(fragment1.onHideCount).isEqualTo(1)
+
+ FragmentTestUtil.popBackStackImmediate(activityRule, id, 0)
+ FragmentTestUtil.assertChildren(container, fragment1)
+
+ assertThat(fragment1.onShowCount).isEqualTo(0)
+ assertThat(fragment1.onHideCount).isEqualTo(1)
+ }
+
+ // hiding and showing the same view should optimize out
+ @Test
+ fun removeRedundantShow() {
+ val fragment1 = CountCallsFragment()
+ val id = fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ assertThat(fragment1.onShowCount).isEqualTo(0)
+ assertThat(fragment1.onHideCount).isEqualTo(0)
+ FragmentTestUtil.assertChildren(container, fragment1)
+
+ instrumentation.runOnMainSync {
+ fm.beginTransaction()
+ .hide(fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.beginTransaction()
+ .detach(fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.beginTransaction()
+ .attach(fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.beginTransaction()
+ .show(fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.executePendingTransactions()
+ }
+
+ FragmentTestUtil.assertChildren(container, fragment1)
+ // can optimize out the show/hide
+ assertThat(fragment1.onShowCount).isEqualTo(0)
+ assertThat(fragment1.onHideCount).isEqualTo(0)
+
+ FragmentTestUtil.popBackStackImmediate(
+ activityRule, id,
+ FragmentManager.POP_BACK_STACK_INCLUSIVE
+ )
+ assertThat(fragment1.onShowCount).isEqualTo(0)
+ assertThat(fragment1.onHideCount).isEqualTo(0)
+ }
+
+ // The View order shouldn't be messed up by reordering -- a view that
+ // is optimized to not remove/add should be in its correct position after
+ // the transaction completes.
+ @Test
+ fun viewOrder() {
+ val fragment1 = CountCallsFragment()
+ val id = fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ FragmentTestUtil.assertChildren(container, fragment1)
+
+ val fragment2 = CountCallsFragment()
+
+ instrumentation.runOnMainSync {
+ fm.beginTransaction()
+ .replace(R.id.fragmentContainer, fragment2)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+
+ fm.executePendingTransactions()
+ }
+ FragmentTestUtil.assertChildren(container, fragment2, fragment1)
+
+ FragmentTestUtil.popBackStackImmediate(activityRule, id, 0)
+ FragmentTestUtil.assertChildren(container, fragment1)
+ }
+
+ // Popping an added transaction results in no operation
+ @Test
+ fun addPopBackStack() {
+ val fragment1 = CountCallsFragment()
+ instrumentation.runOnMainSync {
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.popBackStack()
+ fm.executePendingTransactions()
+ }
+ FragmentTestUtil.assertChildren(container)
+
+ // Was never instantiated because it was popped before anything could happen
+ assertThat(fragment1.onCreateViewCount).isEqualTo(0)
+ }
+
+ // A non-back-stack transaction doesn't interfere with back stack add/pop
+ // optimization.
+ @Test
+ fun popNonBackStack() {
+ val fragment1 = CountCallsFragment()
+ val fragment2 = CountCallsFragment()
+ instrumentation.runOnMainSync {
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.beginTransaction()
+ .replace(R.id.fragmentContainer, fragment2)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.popBackStack()
+ fm.executePendingTransactions()
+ }
+ FragmentTestUtil.assertChildren(container, fragment2)
+
+ // It should be optimized with the replace, so no View creation
+ assertThat(fragment1.onCreateViewCount).isEqualTo(0)
+ }
+
+ // When reordering is disabled, the transaction prior to the disabled reordering
+ // transaction should all be run prior to running the ordered transaction.
+ @Test
+ fun noReordering() {
+ val fragment1 = CountCallsFragment()
+ val fragment2 = CountCallsFragment()
+ instrumentation.runOnMainSync {
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.beginTransaction()
+ .replace(R.id.fragmentContainer, fragment2)
+ .addToBackStack(null)
+ .setReorderingAllowed(false)
+ .commit()
+ fm.executePendingTransactions()
+ }
+ FragmentTestUtil.assertChildren(container, fragment2)
+
+ // No reordering, so fragment1 should have created its View
+ assertThat(fragment1.onCreateViewCount).isEqualTo(1)
+ }
+
+ // Test that a fragment view that is created with focus has focus after the transaction
+ // completes.
+ @UiThreadTest
+ @Test
+ fun focusedView() {
+ activityRule.activity.setContentView(R.layout.double_container)
+ container = activityRule.activity.findViewById<View>(R.id.fragmentContainer1) as ViewGroup
+ val firstEditText = EditText(container.context)
+ container.addView(firstEditText)
+ firstEditText.requestFocus()
+
+ assertThat(firstEditText.isFocused).isTrue()
+ val fragment1 = CountCallsFragment()
+ val fragment2 = CountCallsFragment(R.layout.with_edit_text)
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer2, fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.beginTransaction()
+ .replace(R.id.fragmentContainer2, fragment2)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ fm.executePendingTransactions()
+ val editText = fragment2.requireView().findViewById<View>(R.id.editText)
+ assertThat(editText.isFocused).isTrue()
+ assertThat(firstEditText.isFocused).isFalse()
+ }
+}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/FragmentReplaceTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/FragmentReplaceTest.kt
index 1430927..2284c44 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/FragmentReplaceTest.kt
+++ b/fragment/src/androidTest/java/androidx/fragment/app/FragmentReplaceTest.kt
@@ -19,7 +19,6 @@
import android.view.KeyEvent
import android.view.View
import androidx.fragment.app.test.FragmentTestActivity
-import androidx.fragment.app.test.FragmentTestActivity.TestFragment
import androidx.fragment.test.R
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -53,7 +52,7 @@
val fm = activity.supportFragmentManager
fm.beginTransaction()
- .add(R.id.content, TestFragment.create(R.layout.fragment_a))
+ .add(R.id.content, StrictViewFragment(R.layout.fragment_a))
.addToBackStack(null)
.commit()
executePendingTransactions(fm)
@@ -62,7 +61,7 @@
assertThat(activity.findViewById<View>(R.id.textC)).isNull()
fm.beginTransaction()
- .add(R.id.content, TestFragment.create(R.layout.fragment_b))
+ .add(R.id.content, StrictViewFragment(R.layout.fragment_b))
.addToBackStack(null)
.commit()
executePendingTransactions(fm)
@@ -71,7 +70,7 @@
assertThat(activity.findViewById<View>(R.id.textC)).isNull()
activity.supportFragmentManager.beginTransaction()
- .replace(R.id.content, TestFragment.create(R.layout.fragment_c))
+ .replace(R.id.content, StrictViewFragment(R.layout.fragment_c))
.addToBackStack(null)
.commit()
executePendingTransactions(fm)
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransactionTest.java b/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransactionTest.java
deleted file mode 100644
index bae9332..0000000
--- a/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransactionTest.java
+++ /dev/null
@@ -1,460 +0,0 @@
-/*
- * 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 androidx.fragment.app;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotSame;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.app.Activity;
-import android.app.Instrumentation;
-import android.content.Intent;
-import android.os.Bundle;
-import android.os.SystemClock;
-import android.view.LayoutInflater;
-
-import androidx.annotation.ContentView;
-import androidx.annotation.NonNull;
-import androidx.fragment.app.test.FragmentTestActivity;
-import androidx.fragment.app.test.NewIntentActivity;
-import androidx.fragment.test.R;
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.MediumTest;
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.rule.ActivityTestRule;
-
-import org.junit.After;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.Collection;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Tests usage of the {@link FragmentTransaction} class.
- */
-@MediumTest
-@RunWith(AndroidJUnit4.class)
-public class FragmentTransactionTest {
-
- @Rule
- public ActivityTestRule<FragmentTestActivity> mActivityRule =
- new ActivityTestRule<>(FragmentTestActivity.class);
-
- private FragmentTestActivity mActivity;
- private int mOnBackStackChangedTimes;
- private FragmentManager.OnBackStackChangedListener mOnBackStackChangedListener;
-
- @Before
- public void setUp() {
- mActivity = mActivityRule.getActivity();
- mOnBackStackChangedTimes = 0;
- mOnBackStackChangedListener = new FragmentManager.OnBackStackChangedListener() {
- @Override
- public void onBackStackChanged() {
- mOnBackStackChangedTimes++;
- }
- };
- mActivity.getSupportFragmentManager()
- .addOnBackStackChangedListener(mOnBackStackChangedListener);
- }
-
- @After
- public void tearDown() {
- mActivity.getSupportFragmentManager()
- .removeOnBackStackChangedListener(mOnBackStackChangedListener);
- mOnBackStackChangedListener = null;
- }
-
- @Test
- @UiThreadTest
- public void testAddTransactionWithValidFragment() {
- final Fragment fragment = new CorrectFragment();
- mActivity.getSupportFragmentManager().beginTransaction()
- .add(R.id.content, fragment)
- .addToBackStack(null)
- .commit();
- mActivity.getSupportFragmentManager().executePendingTransactions();
- assertEquals(1, mOnBackStackChangedTimes);
- assertTrue(fragment.isAdded());
- }
-
- @Test
- @UiThreadTest
- public void testAddTransactionWithPrivateFragment() {
- final Fragment fragment = new PrivateFragment();
- boolean exceptionThrown = false;
- try {
- mActivity.getSupportFragmentManager().beginTransaction()
- .add(R.id.content, fragment)
- .addToBackStack(null)
- .commit();
- mActivity.getSupportFragmentManager().executePendingTransactions();
- assertEquals(1, mOnBackStackChangedTimes);
- } catch (IllegalStateException e) {
- exceptionThrown = true;
- } finally {
- assertTrue("Exception should be thrown", exceptionThrown);
- assertFalse("Fragment shouldn't be added", fragment.isAdded());
- }
- }
-
- @Test
- @UiThreadTest
- public void testAddTransactionWithPackagePrivateFragment() {
- final Fragment fragment = new PackagePrivateFragment();
- boolean exceptionThrown = false;
- try {
- mActivity.getSupportFragmentManager().beginTransaction()
- .add(R.id.content, fragment)
- .addToBackStack(null)
- .commit();
- mActivity.getSupportFragmentManager().executePendingTransactions();
- assertEquals(1, mOnBackStackChangedTimes);
- } catch (IllegalStateException e) {
- exceptionThrown = true;
- } finally {
- assertTrue("Exception should be thrown", exceptionThrown);
- assertFalse("Fragment shouldn't be added", fragment.isAdded());
- }
- }
-
- @Test
- @UiThreadTest
- public void testAddTransactionWithAnonymousFragment() {
- final Fragment fragment = new Fragment() {};
- boolean exceptionThrown = false;
- try {
- mActivity.getSupportFragmentManager().beginTransaction()
- .add(R.id.content, fragment)
- .addToBackStack(null)
- .commit();
- mActivity.getSupportFragmentManager().executePendingTransactions();
- assertEquals(1, mOnBackStackChangedTimes);
- } catch (IllegalStateException e) {
- exceptionThrown = true;
- } finally {
- assertTrue("Exception should be thrown", exceptionThrown);
- assertFalse("Fragment shouldn't be added", fragment.isAdded());
- }
- }
-
- @Test
- @UiThreadTest
- public void testGetLayoutInflater() {
- final OnGetLayoutInflaterFragment fragment1 = new OnGetLayoutInflaterFragment();
- assertEquals(0, fragment1.onGetLayoutInflaterCalls);
- mActivity.getSupportFragmentManager().beginTransaction()
- .add(R.id.content, fragment1)
- .addToBackStack(null)
- .commit();
- mActivity.getSupportFragmentManager().executePendingTransactions();
- assertEquals(1, fragment1.onGetLayoutInflaterCalls);
- assertEquals(fragment1.layoutInflater, fragment1.getLayoutInflater());
- // getLayoutInflater() didn't force onGetLayoutInflater()
- assertEquals(1, fragment1.onGetLayoutInflaterCalls);
-
- LayoutInflater layoutInflater = fragment1.layoutInflater;
- // Replacing fragment1 won't detach it, so the value won't be cleared
- final OnGetLayoutInflaterFragment fragment2 = new OnGetLayoutInflaterFragment();
- mActivity.getSupportFragmentManager().beginTransaction()
- .replace(R.id.content, fragment2)
- .addToBackStack(null)
- .commit();
- mActivity.getSupportFragmentManager().executePendingTransactions();
-
- assertSame(layoutInflater, fragment1.getLayoutInflater());
- assertEquals(1, fragment1.onGetLayoutInflaterCalls);
-
- // Popping it should cause onCreateView again, so a new LayoutInflater...
- mActivity.getSupportFragmentManager().popBackStackImmediate();
- assertNotSame(layoutInflater, fragment1.getLayoutInflater());
- assertEquals(2, fragment1.onGetLayoutInflaterCalls);
- layoutInflater = fragment1.layoutInflater;
- assertSame(layoutInflater, fragment1.getLayoutInflater());
-
- // Popping it should detach it, clearing the cached value again
- mActivity.getSupportFragmentManager().popBackStackImmediate();
-
- // once it is detached, the getLayoutInflater() will default to throw
- // an exception, but we've made it return null instead.
- assertEquals(2, fragment1.onGetLayoutInflaterCalls);
- try {
- fragment1.getLayoutInflater();
- fail("getLayoutInflater should throw when the Fragment is detached");
- } catch (IllegalStateException e) {
- // Expected
- }
- assertEquals(3, fragment1.onGetLayoutInflaterCalls);
- }
-
- @Test
- @UiThreadTest
- public void testAddTransactionWithNonStaticFragment() {
- final Fragment fragment = new NonStaticFragment();
- boolean exceptionThrown = false;
- try {
- mActivity.getSupportFragmentManager().beginTransaction()
- .add(R.id.content, fragment)
- .addToBackStack(null)
- .commit();
- mActivity.getSupportFragmentManager().executePendingTransactions();
- assertEquals(1, mOnBackStackChangedTimes);
- } catch (IllegalStateException e) {
- exceptionThrown = true;
- } finally {
- assertTrue("Exception should be thrown", exceptionThrown);
- assertFalse("Fragment shouldn't be added", fragment.isAdded());
- }
- }
-
- @Test
- @UiThreadTest
- public void testPostOnCommit() {
- final boolean[] ran = new boolean[1];
- FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- fm.beginTransaction().runOnCommit(new Runnable() {
- @Override
- public void run() {
- ran[0] = true;
- }
- }).commit();
- fm.executePendingTransactions();
-
- assertTrue("runOnCommit runnable never ran", ran[0]);
-
- ran[0] = false;
-
- boolean threw = false;
- try {
- fm.beginTransaction().runOnCommit(new Runnable() {
- @Override
- public void run() {
- ran[0] = true;
- }
- }).addToBackStack(null).commit();
- } catch (IllegalStateException ise) {
- threw = true;
- }
-
- fm.executePendingTransactions();
-
- assertTrue("runOnCommit was allowed to be called for back stack transaction",
- threw);
- assertFalse("runOnCommit runnable for back stack transaction was run", ran[0]);
- }
-
- // Ensure that getFragments() works during transactions, even if it is run off thread
- @Test
- public void getFragmentsOffThread() throws Throwable {
- final FragmentManager fm = mActivity.getSupportFragmentManager();
-
- // Make sure that adding a fragment works
- Fragment fragment = new CorrectFragment();
- fm.beginTransaction()
- .add(R.id.content, fragment)
- .addToBackStack(null)
- .commit();
-
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- Collection<Fragment> fragments = fm.getFragments();
- assertEquals(1, fragments.size());
- assertTrue(fragments.contains(fragment));
-
- // Removed fragments shouldn't show
- fm.beginTransaction()
- .remove(fragment)
- .addToBackStack(null)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- assertTrue(fm.getFragments().isEmpty());
-
- // Now try detached fragments
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
- fm.beginTransaction()
- .detach(fragment)
- .addToBackStack(null)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- assertTrue(fm.getFragments().isEmpty());
-
- // Now try hidden fragments
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
- fm.beginTransaction()
- .hide(fragment)
- .addToBackStack(null)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- fragments = fm.getFragments();
- assertEquals(1, fragments.size());
- assertTrue(fragments.contains(fragment));
-
- // And showing it again shouldn't change anything:
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
- fragments = fm.getFragments();
- assertEquals(1, fragments.size());
- assertTrue(fragments.contains(fragment));
-
- // Now pop back to the start state
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
-
- // We can't force concurrency, but we can do it lots of times and hope that
- // we hit it.
- // Reset count here to verify afterwards
-
- // Wait until we receive a OnBackStackChange callback for the total number of times
- // specified by transactionCount times 2 (1 for adding, 1 for removal)
- final int transactionCount = 100;
- final CountDownLatch backStackLatch = new CountDownLatch(transactionCount * 2);
- final FragmentManager.OnBackStackChangedListener countDownListener =
- new FragmentManager.OnBackStackChangedListener() {
-
- @Override
- public void onBackStackChanged() {
- backStackLatch.countDown();
- }
- };
-
- fm.addOnBackStackChangedListener(countDownListener);
-
- for (int i = 0; i < transactionCount; i++) {
- Fragment fragment2 = new CorrectFragment();
- fm.beginTransaction()
- .add(R.id.content, fragment2)
- .addToBackStack(null)
- .commit();
- getFragmentsUntilSize(1);
-
- fm.popBackStack();
- getFragmentsUntilSize(0);
- }
-
- backStackLatch.await();
-
- fm.removeOnBackStackChangedListener(countDownListener);
- }
-
- /**
- * When a FragmentManager is detached, it should allow commitAllowingStateLoss()
- * and commitNowAllowingStateLoss() by just dropping the transaction.
- */
- @Test
- public void commitAllowStateLossDetached() throws Throwable {
- Fragment fragment1 = new CorrectFragment();
- mActivity.getSupportFragmentManager()
- .beginTransaction()
- .add(fragment1, "1")
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- final FragmentManager fm = fragment1.getChildFragmentManager();
- mActivity.getSupportFragmentManager()
- .beginTransaction()
- .remove(fragment1)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- Assert.assertEquals(0, mActivity.getSupportFragmentManager().getFragments().size());
- assertEquals(0, fm.getFragments().size());
-
- // Now the fragment1's fragment manager should allow commitAllowingStateLoss
- // by doing nothing since it has been detached.
- Fragment fragment2 = new CorrectFragment();
- fm.beginTransaction()
- .add(fragment2, "2")
- .commitAllowingStateLoss();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- assertEquals(0, fm.getFragments().size());
-
- // It should also allow commitNowAllowingStateLoss by doing nothing
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- Fragment fragment3 = new CorrectFragment();
- fm.beginTransaction()
- .add(fragment3, "3")
- .commitNowAllowingStateLoss();
- assertEquals(0, fm.getFragments().size());
- }
- });
- }
-
- /**
- * onNewIntent() should note that the state is not saved so that child fragment
- * managers can execute transactions.
- */
- @Test
- public void newIntentUnlocks() throws Throwable {
- Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
- Intent intent1 = new Intent(mActivity, NewIntentActivity.class)
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- NewIntentActivity newIntentActivity =
- (NewIntentActivity) instrumentation.startActivitySync(intent1);
- FragmentTestUtil.waitForExecution(mActivityRule);
-
- Intent intent2 = new Intent(mActivity, FragmentTestActivity.class);
- intent2.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- Activity coveringActivity = instrumentation.startActivitySync(intent2);
- FragmentTestUtil.waitForExecution(mActivityRule);
-
- Intent intent3 = new Intent(mActivity, NewIntentActivity.class)
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- mActivity.startActivity(intent3);
- assertTrue(newIntentActivity.newIntent.await(1, TimeUnit.SECONDS));
- FragmentTestUtil.waitForExecution(mActivityRule);
-
- for (Fragment fragment : newIntentActivity.getSupportFragmentManager().getFragments()) {
- // There really should only be one fragment in newIntentActivity.
- assertEquals(1, fragment.getChildFragmentManager().getFragments().size());
- }
- }
-
- private void getFragmentsUntilSize(int expectedSize) {
- final long endTime = SystemClock.uptimeMillis() + 3000;
-
- do {
- assertTrue(SystemClock.uptimeMillis() < endTime);
- } while (mActivity.getSupportFragmentManager().getFragments().size() != expectedSize);
- }
-
- public static class CorrectFragment extends Fragment {}
-
- private static class PrivateFragment extends Fragment {}
-
- static class PackagePrivateFragment extends Fragment {}
-
- private class NonStaticFragment extends Fragment {}
-
- @ContentView(R.layout.fragment_a)
- public static class OnGetLayoutInflaterFragment extends Fragment {
- public int onGetLayoutInflaterCalls = 0;
- public LayoutInflater layoutInflater;
-
- @NonNull
- @Override
- public LayoutInflater onGetLayoutInflater(Bundle savedInstanceState) {
- onGetLayoutInflaterCalls++;
- layoutInflater = super.onGetLayoutInflater(savedInstanceState);
- return layoutInflater;
- }
- }
-}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransactionTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransactionTest.kt
new file mode 100644
index 0000000..0c1e3fb
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransactionTest.kt
@@ -0,0 +1,421 @@
+/*
+ * 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 androidx.fragment.app.test
+
+import org.junit.Assert.fail
+
+import android.content.Intent
+import android.os.Bundle
+import android.os.SystemClock
+import android.view.LayoutInflater
+
+import androidx.annotation.ContentView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentTestUtil
+import androidx.fragment.test.R
+import androidx.test.annotation.UiThreadTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.ActivityTestRule
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+/**
+ * Tests usage of the [FragmentTransaction] class.
+ */
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class FragmentTransactionTest {
+
+ @get:Rule
+ var activityRule = ActivityTestRule(FragmentTestActivity::class.java)
+
+ private lateinit var activity: FragmentTestActivity
+ private var onBackStackChangedTimes: Int = 0
+ private lateinit var onBackStackChangedListener: FragmentManager.OnBackStackChangedListener
+
+ @Before
+ fun setUp() {
+ activity = activityRule.activity
+ onBackStackChangedTimes = 0
+ onBackStackChangedListener =
+ FragmentManager.OnBackStackChangedListener { onBackStackChangedTimes++ }
+ activity.supportFragmentManager.addOnBackStackChangedListener(onBackStackChangedListener)
+ }
+
+ @After
+ fun tearDown() {
+ activity.supportFragmentManager.removeOnBackStackChangedListener(onBackStackChangedListener)
+ }
+
+ @Test
+ @UiThreadTest
+ fun testAddTransactionWithValidFragment() {
+ val fragment = CorrectFragment()
+ activity.supportFragmentManager.beginTransaction()
+ .add(R.id.content, fragment)
+ .addToBackStack(null)
+ .commit()
+ activity.supportFragmentManager.executePendingTransactions()
+ assertThat(onBackStackChangedTimes).isEqualTo(1)
+ assertThat(fragment.isAdded).isTrue()
+ }
+
+ @Test
+ @UiThreadTest
+ fun testAddTransactionWithPrivateFragment() {
+ val fragment = PrivateFragment()
+ try {
+ activity.supportFragmentManager.beginTransaction()
+ .add(R.id.content, fragment)
+ .addToBackStack(null)
+ .commit()
+ activity.supportFragmentManager.executePendingTransactions()
+ assertThat(onBackStackChangedTimes).isEqualTo(1)
+ } catch (e: IllegalStateException) {
+ assertThat(e)
+ .hasMessageThat().contains("Fragment " + fragment.javaClass.canonicalName +
+ " must be a public static class to be properly recreated from instance " +
+ "state.")
+ } finally {
+ assertWithMessage("Fragment shouldn't be added").that(fragment.isAdded).isFalse()
+ }
+ }
+
+ @Test
+ @UiThreadTest
+ fun testAddTransactionWithPackagePrivateFragment() {
+ val fragment = OuterPackagePrivateFragment.PackagePrivateFragment()
+ try {
+ activity.supportFragmentManager.beginTransaction()
+ .add(R.id.content, fragment)
+ .addToBackStack(null)
+ .commit()
+ activity.supportFragmentManager.executePendingTransactions()
+ assertThat(onBackStackChangedTimes).isEqualTo(1)
+ } catch (e: IllegalStateException) {
+ assertThat(e).hasMessageThat().contains("Fragment " + fragment.javaClass.canonicalName +
+ " must be a public static class to be properly recreated from instance " +
+ "state.")
+ } finally {
+ assertWithMessage("Fragment shouldn't be added").that(fragment.isAdded).isFalse()
+ }
+ }
+
+ @Test
+ @UiThreadTest
+ fun testAddTransactionWithAnonymousFragment() {
+ val fragment = object : Fragment() {}
+ try {
+ activity.supportFragmentManager.beginTransaction()
+ .add(R.id.content, fragment)
+ .addToBackStack(null)
+ .commit()
+ activity.supportFragmentManager.executePendingTransactions()
+ assertThat(onBackStackChangedTimes).isEqualTo(1)
+ } catch (e: IllegalStateException) {
+ assertThat(e).hasMessageThat().contains("Fragment " + fragment.javaClass.canonicalName +
+ " must be a public static class to be properly recreated from instance state.")
+ } finally {
+ assertWithMessage("Fragment shouldn't be added").that(fragment.isAdded).isFalse()
+ }
+ }
+
+ @Test
+ @UiThreadTest
+ fun testGetLayoutInflater() {
+ val fragment1 = OnGetLayoutInflaterFragment()
+ assertThat(fragment1.onGetLayoutInflaterCalls).isEqualTo(0)
+ activity.supportFragmentManager.beginTransaction()
+ .add(R.id.content, fragment1)
+ .addToBackStack(null)
+ .commit()
+ activity.supportFragmentManager.executePendingTransactions()
+ assertThat(fragment1.onGetLayoutInflaterCalls).isEqualTo(1)
+ assertThat(fragment1.layoutInflater).isEqualTo(fragment1.baseLayoutInflater)
+ // getBaseLayoutInflater() didn't force onGetLayoutInflater()
+ assertThat(fragment1.onGetLayoutInflaterCalls).isEqualTo(1)
+
+ var layoutInflater = fragment1.baseLayoutInflater
+ // Replacing fragment1 won't detach it, so the value won't be cleared
+ val fragment2 = OnGetLayoutInflaterFragment()
+ activity.supportFragmentManager.beginTransaction()
+ .replace(R.id.content, fragment2)
+ .addToBackStack(null)
+ .commit()
+ activity.supportFragmentManager.executePendingTransactions()
+
+ assertThat(fragment1.layoutInflater).isSameAs(layoutInflater)
+ assertThat(fragment1.onGetLayoutInflaterCalls).isEqualTo(1)
+
+ // Popping it should cause onCreateView again, so a new LayoutInflater...
+ activity.supportFragmentManager.popBackStackImmediate()
+ assertThat(fragment1.layoutInflater).isNotSameAs(layoutInflater)
+ assertThat(fragment1.onGetLayoutInflaterCalls).isEqualTo(2)
+ layoutInflater = fragment1.baseLayoutInflater
+ assertThat(fragment1.layoutInflater).isSameAs(layoutInflater)
+
+ // Popping it should detach it, clearing the cached value again
+ activity.supportFragmentManager.popBackStackImmediate()
+
+ // once it is detached, the getBaseLayoutInflater() will default to throw
+ // an exception, but we've made it return null instead.
+ assertThat(fragment1.onGetLayoutInflaterCalls).isEqualTo(2)
+ try {
+ fragment1.layoutInflater
+ fail("getLayoutInflater should throw when the Fragment is detached")
+ } catch (e: IllegalStateException) {
+ assertThat(e).hasMessageThat().contains("onGetLayoutInflater() cannot be executed " +
+ "until the Fragment is attached to the FragmentManager.")
+ }
+
+ assertThat(fragment1.onGetLayoutInflaterCalls).isEqualTo(3)
+ }
+
+ @Test
+ @UiThreadTest
+ fun testAddTransactionWithNonStaticFragment() {
+ val fragment = NonStaticFragment()
+ try {
+ activity.supportFragmentManager.beginTransaction()
+ .add(R.id.content, fragment)
+ .addToBackStack(null)
+ .commit()
+ activity.supportFragmentManager.executePendingTransactions()
+ assertThat(onBackStackChangedTimes).isEqualTo(1)
+ } catch (e: IllegalStateException) {
+ assertThat(e).hasMessageThat().contains("Fragment " + fragment.javaClass.canonicalName +
+ " must be a public static class to be properly recreated from instance state.")
+ } finally {
+ assertWithMessage("Fragment shouldn't be added").that(fragment.isAdded).isFalse()
+ }
+ }
+
+ @Test
+ @UiThreadTest
+ fun testPostOnCommit() {
+ var ran = false
+ val fm = activityRule.activity.supportFragmentManager
+ fm.beginTransaction().runOnCommit { ran = true }.commit()
+ fm.executePendingTransactions()
+
+ assertWithMessage("runOnCommit runnable never ran").that(ran).isTrue()
+
+ ran = false
+
+ try {
+ fm.beginTransaction().runOnCommit { ran = true }.addToBackStack(null).commit()
+ } catch (e: IllegalStateException) {
+ assertThat(e).hasMessageThat().contains("This FragmentTransaction is not allowed to" +
+ " be added to the back stack.")
+ }
+
+ fm.executePendingTransactions()
+
+ assertWithMessage("runOnCommit runnable for back stack transaction was run")
+ .that(ran)
+ .isFalse()
+ }
+
+ // Ensure that getFragments() works during transactions, even if it is run off thread
+ @Test
+ fun getFragmentsOffThread() {
+ val fm = activity.supportFragmentManager
+
+ // Make sure that adding a fragment works
+ val fragment = CorrectFragment()
+ fm.beginTransaction()
+ .add(R.id.content, fragment)
+ .addToBackStack(null)
+ .commit()
+
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ var fragments: Collection<Fragment> = fm.fragments
+ assertThat(fragments.size).isEqualTo(1)
+ assertThat(fragments.contains(fragment)).isTrue()
+
+ // Removed fragments shouldn't show
+ fm.beginTransaction()
+ .remove(fragment)
+ .addToBackStack(null)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ assertThat(fm.fragments.isEmpty()).isTrue()
+
+ // Now try detached fragments
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+ fm.beginTransaction()
+ .detach(fragment)
+ .addToBackStack(null)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ assertThat(fm.fragments.isEmpty()).isTrue()
+
+ // Now try hidden fragments
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+ fm.beginTransaction()
+ .hide(fragment)
+ .addToBackStack(null)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ fragments = fm.fragments
+ assertThat(fragments.size).isEqualTo(1)
+ assertThat(fragments.contains(fragment)).isTrue()
+
+ // And showing it again shouldn't change anything:
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+ fragments = fm.fragments
+ assertThat(fragments.size).isEqualTo(1)
+ assertThat(fragments.contains(fragment)).isTrue()
+
+ // Now pop back to the start state
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+
+ // We can't force concurrency, but we can do it lots of times and hope that
+ // we hit it.
+ // Reset count here to verify afterwards
+
+ // Wait until we receive a OnBackStackChange callback for the total number of times
+ // specified by transactionCount times 2 (1 for adding, 1 for removal)
+ val transactionCount = 100
+ val backStackLatch = CountDownLatch(transactionCount * 2)
+ val countDownListener =
+ FragmentManager.OnBackStackChangedListener { backStackLatch.countDown() }
+
+ fm.addOnBackStackChangedListener(countDownListener)
+
+ for (i in 0 until transactionCount) {
+ val fragment2 = CorrectFragment()
+ fm.beginTransaction()
+ .add(R.id.content, fragment2)
+ .addToBackStack(null)
+ .commit()
+ getFragmentsUntilSize(1)
+
+ fm.popBackStack()
+ getFragmentsUntilSize(0)
+ }
+
+ backStackLatch.await()
+
+ fm.removeOnBackStackChangedListener(countDownListener)
+ }
+
+ /**
+ * When a FragmentManager is detached, it should allow commitAllowingStateLoss()
+ * and commitNowAllowingStateLoss() by just dropping the transaction.
+ */
+ @Test
+ fun commitAllowStateLossDetached() {
+ val fragment1 = CorrectFragment()
+ activity.supportFragmentManager
+ .beginTransaction()
+ .add(fragment1, "1")
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ val fm = fragment1.childFragmentManager
+ activity.supportFragmentManager
+ .beginTransaction()
+ .remove(fragment1)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ assertThat(activity.supportFragmentManager.fragments.size).isEqualTo(0)
+ assertThat(fm.fragments.size).isEqualTo(0)
+
+ // Now the fragment1's fragment manager should allow commitAllowingStateLoss
+ // by doing nothing since it has been detached.
+ val fragment2 = CorrectFragment()
+ fm.beginTransaction()
+ .add(fragment2, "2")
+ .commitAllowingStateLoss()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ assertThat(fm.fragments.size).isEqualTo(0)
+
+ // It should also allow commitNowAllowingStateLoss by doing nothing
+ activityRule.runOnUiThread {
+ val fragment3 = CorrectFragment()
+ fm.beginTransaction()
+ .add(fragment3, "3")
+ .commitNowAllowingStateLoss()
+ assertThat(fm.fragments.size).isEqualTo(0)
+ }
+ }
+
+ /**
+ * onNewIntent() should note that the state is not saved so that child fragment
+ * managers can execute transactions.
+ */
+ @Test
+ fun newIntentUnlocks() {
+ val instrumentation = InstrumentationRegistry.getInstrumentation()
+ val intent1 = Intent(activity, NewIntentActivity::class.java)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ val newIntentActivity = instrumentation.startActivitySync(intent1) as NewIntentActivity
+ FragmentTestUtil.waitForExecution(activityRule)
+
+ val intent2 = Intent(activity, FragmentTestActivity::class.java)
+ intent2.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ instrumentation.startActivitySync(intent2)
+ FragmentTestUtil.waitForExecution(activityRule)
+
+ val intent3 = Intent(activity, NewIntentActivity::class.java)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ activity.startActivity(intent3)
+ assertThat(newIntentActivity.newIntent.await(1, TimeUnit.SECONDS)).isTrue()
+ FragmentTestUtil.waitForExecution(activityRule)
+
+ for (fragment in newIntentActivity.supportFragmentManager.fragments) {
+ // There really should only be one fragment in newIntentActivity.
+ assertThat(fragment.childFragmentManager.fragments.size).isEqualTo(1)
+ }
+ }
+
+ private fun getFragmentsUntilSize(expectedSize: Int) {
+ val endTime = SystemClock.uptimeMillis() + 3000
+
+ do {
+ assertThat(SystemClock.uptimeMillis() < endTime).isTrue()
+ } while (activity.supportFragmentManager.fragments.size != expectedSize)
+ }
+
+ class CorrectFragment : Fragment()
+
+ private class PrivateFragment : Fragment()
+
+ private inner class NonStaticFragment : Fragment()
+
+ @ContentView(R.layout.fragment_a)
+ class OnGetLayoutInflaterFragment : Fragment() {
+ var onGetLayoutInflaterCalls = 0
+ lateinit var baseLayoutInflater: LayoutInflater
+
+ override fun onGetLayoutInflater(savedInstanceState: Bundle?): LayoutInflater {
+ onGetLayoutInflaterCalls++
+ baseLayoutInflater = super.onGetLayoutInflater(savedInstanceState)
+ return baseLayoutInflater
+ }
+ }
+}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionTest.java b/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionTest.java
deleted file mode 100644
index 04cb401..0000000
--- a/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionTest.java
+++ /dev/null
@@ -1,1155 +0,0 @@
-/*
- * 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 androidx.fragment.app;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.verify;
-
-import android.app.Instrumentation;
-import android.graphics.Rect;
-import android.os.Build;
-import android.os.Bundle;
-import android.transition.TransitionSet;
-import android.view.View;
-
-import androidx.core.app.SharedElementCallback;
-import androidx.fragment.app.test.FragmentTestActivity;
-import androidx.fragment.test.R;
-import androidx.test.filters.MediumTest;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.rule.ActivityTestRule;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.mockito.ArgumentCaptor;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-@MediumTest
-@RunWith(Parameterized.class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
-public class FragmentTransitionTest {
- private final boolean mReorderingAllowed;
-
- @Parameterized.Parameters
- public static Object[] data() {
- return new Boolean[] {
- false, true
- };
- }
-
- @Rule
- public ActivityTestRule<FragmentTestActivity> mActivityRule =
- new ActivityTestRule<FragmentTestActivity>(FragmentTestActivity.class);
-
- private Instrumentation mInstrumentation;
- private FragmentManager mFragmentManager;
- private int mOnBackStackChangedTimes;
- private FragmentManager.OnBackStackChangedListener mOnBackStackChangedListener;
-
- public FragmentTransitionTest(final boolean reordering) {
- mReorderingAllowed = reordering;
- }
-
- @Before
- public void setup() throws Throwable {
- mInstrumentation = InstrumentationRegistry.getInstrumentation();
- mFragmentManager = mActivityRule.getActivity().getSupportFragmentManager();
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- mOnBackStackChangedTimes = 0;
- mOnBackStackChangedListener = new FragmentManager.OnBackStackChangedListener() {
- @Override
- public void onBackStackChanged() {
- mOnBackStackChangedTimes++;
- }
- };
- mFragmentManager.addOnBackStackChangedListener(mOnBackStackChangedListener);
- }
-
- @After
- public void teardown() throws Throwable {
- mFragmentManager.removeOnBackStackChangedListener(mOnBackStackChangedListener);
- mOnBackStackChangedListener = null;
- }
-
- // Test that normal view transitions (enter, exit, reenter, return) run with
- // a single fragment.
- @Test
- public void enterExitTransitions() throws Throwable {
- // enter transition
- TransitionFragment fragment = setupInitialFragment();
- final View blue = findBlue();
- final View green = findBlue();
-
- // exit transition
- mFragmentManager.beginTransaction()
- .setReorderingAllowed(mReorderingAllowed)
- .remove(fragment)
- .addToBackStack(null)
- .commit();
-
- fragment.waitForTransition();
- verifyAndClearTransition(fragment.exitTransition, null, green, blue);
- verifyNoOtherTransitions(fragment);
- assertEquals(2, mOnBackStackChangedTimes);
-
- // reenter transition
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
- fragment.waitForTransition();
- final View green2 = findGreen();
- final View blue2 = findBlue();
- verifyAndClearTransition(fragment.reenterTransition, null, green2, blue2);
- verifyNoOtherTransitions(fragment);
- assertEquals(3, mOnBackStackChangedTimes);
-
- // return transition
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
- fragment.waitForTransition();
- verifyAndClearTransition(fragment.returnTransition, null, green2, blue2);
- verifyNoOtherTransitions(fragment);
- assertEquals(4, mOnBackStackChangedTimes);
- }
-
- // Test that shared elements transition from one fragment to the next
- // and back during pop.
- @Test
- public void sharedElement() throws Throwable {
- TransitionFragment fragment1 = setupInitialFragment();
-
- // Now do a transition to scene2
- TransitionFragment fragment2 = new TransitionFragment();
- fragment2.setLayoutId(R.layout.scene2);
-
- verifyTransition(fragment1, fragment2, "blueSquare");
-
- // Now pop the back stack
- verifyPopTransition(1, fragment2, fragment1);
- }
-
- // Test that shared element transitions through multiple fragments work together
- @Test
- public void intermediateFragment() throws Throwable {
- TransitionFragment fragment1 = setupInitialFragment();
-
- final TransitionFragment fragment2 = new TransitionFragment();
- fragment2.setLayoutId(R.layout.scene3);
-
- verifyTransition(fragment1, fragment2, "shared");
-
- final TransitionFragment fragment3 = new TransitionFragment();
- fragment3.setLayoutId(R.layout.scene2);
-
- verifyTransition(fragment2, fragment3, "blueSquare");
-
- // Should transfer backwards when popping multiple:
- verifyPopTransition(2, fragment3, fragment1, fragment2);
- }
-
- // Adding/removing the same fragment multiple times shouldn't mess anything up
- @Test
- public void removeAdded() throws Throwable {
- final TransitionFragment fragment1 = setupInitialFragment();
-
- final View startBlue = findBlue();
- final View startGreen = findGreen();
-
- final TransitionFragment fragment2 = new TransitionFragment();
- fragment2.setLayoutId(R.layout.scene2);
-
- mInstrumentation.runOnMainSync(new Runnable() {
- @Override
- public void run() {
- mFragmentManager.beginTransaction()
- .setReorderingAllowed(mReorderingAllowed)
- .replace(R.id.fragmentContainer, fragment2)
- .replace(R.id.fragmentContainer, fragment1)
- .replace(R.id.fragmentContainer, fragment2)
- .addToBackStack(null)
- .commit();
- }
- });
- FragmentTestUtil.waitForExecution(mActivityRule);
- assertEquals(2, mOnBackStackChangedTimes);
-
- // should be a normal transition from fragment1 to fragment2
- fragment2.waitForTransition();
- final View endBlue = findBlue();
- final View endGreen = findGreen();
- verifyAndClearTransition(fragment1.exitTransition, null, startBlue, startGreen);
- verifyAndClearTransition(fragment2.enterTransition, null, endBlue, endGreen);
- verifyNoOtherTransitions(fragment1);
- verifyNoOtherTransitions(fragment2);
-
- // Pop should also do the same thing
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
- assertEquals(3, mOnBackStackChangedTimes);
-
- fragment1.waitForTransition();
- final View popBlue = findBlue();
- final View popGreen = findGreen();
- verifyAndClearTransition(fragment1.reenterTransition, null, popBlue, popGreen);
- verifyAndClearTransition(fragment2.returnTransition, null, endBlue, endGreen);
- verifyNoOtherTransitions(fragment1);
- verifyNoOtherTransitions(fragment2);
- }
-
- // Make sure that shared elements on two different fragment containers don't interact
- @Test
- public void crossContainer() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.double_container);
- TransitionFragment fragment1 = new TransitionFragment();
- fragment1.setLayoutId(R.layout.scene1);
- TransitionFragment fragment2 = new TransitionFragment();
- fragment2.setLayoutId(R.layout.scene1);
- mFragmentManager.beginTransaction()
- .setReorderingAllowed(mReorderingAllowed)
- .add(R.id.fragmentContainer1, fragment1)
- .add(R.id.fragmentContainer2, fragment2)
- .addToBackStack(null)
- .commit();
- FragmentTestUtil.waitForExecution(mActivityRule);
- assertEquals(1, mOnBackStackChangedTimes);
-
- fragment1.waitForTransition();
- final View greenSquare1 = findViewById(fragment1, R.id.greenSquare);
- final View blueSquare1 = findViewById(fragment1, R.id.blueSquare);
- verifyAndClearTransition(fragment1.enterTransition, null, greenSquare1, blueSquare1);
- verifyNoOtherTransitions(fragment1);
- fragment2.waitForTransition();
- final View greenSquare2 = findViewById(fragment2, R.id.greenSquare);
- final View blueSquare2 = findViewById(fragment2, R.id.blueSquare);
- verifyAndClearTransition(fragment2.enterTransition, null, greenSquare2, blueSquare2);
- verifyNoOtherTransitions(fragment2);
-
- // Make sure the correct transitions are run when the target names
- // are different in both shared elements. We may fool the system.
- verifyCrossTransition(false, fragment1, fragment2);
-
- // Make sure the correct transitions are run when the source names
- // are different in both shared elements. We may fool the system.
- verifyCrossTransition(true, fragment1, fragment2);
- }
-
- // Make sure that onSharedElementStart and onSharedElementEnd are called
- @Test
- public void callStartEndWithSharedElements() throws Throwable {
- TransitionFragment fragment1 = setupInitialFragment();
-
- // Now do a transition to scene2
- TransitionFragment fragment2 = new TransitionFragment();
- fragment2.setLayoutId(R.layout.scene2);
-
- SharedElementCallback enterCallback = mock(SharedElementCallback.class);
- fragment2.setEnterSharedElementCallback(enterCallback);
-
- final View startBlue = findBlue();
-
- verifyTransition(fragment1, fragment2, "blueSquare");
-
- ArgumentCaptor<List> names = ArgumentCaptor.forClass(List.class);
- ArgumentCaptor<List> views = ArgumentCaptor.forClass(List.class);
- ArgumentCaptor<List> snapshots = ArgumentCaptor.forClass(List.class);
- verify(enterCallback).onSharedElementStart(names.capture(), views.capture(),
- snapshots.capture());
- assertEquals(1, names.getValue().size());
- assertEquals(1, views.getValue().size());
- assertNull(snapshots.getValue());
- assertEquals("blueSquare", names.getValue().get(0));
- assertEquals(startBlue, views.getValue().get(0));
-
- final View endBlue = findBlue();
-
- verify(enterCallback).onSharedElementEnd(names.capture(), views.capture(),
- snapshots.capture());
- assertEquals(1, names.getValue().size());
- assertEquals(1, views.getValue().size());
- assertNull(snapshots.getValue());
- assertEquals("blueSquare", names.getValue().get(0));
- assertEquals(endBlue, views.getValue().get(0));
-
- // Now pop the back stack
- reset(enterCallback);
- verifyPopTransition(1, fragment2, fragment1);
-
- verify(enterCallback).onSharedElementStart(names.capture(), views.capture(),
- snapshots.capture());
- assertEquals(1, names.getValue().size());
- assertEquals(1, views.getValue().size());
- assertNull(snapshots.getValue());
- assertEquals("blueSquare", names.getValue().get(0));
- assertEquals(endBlue, views.getValue().get(0));
-
- final View reenterBlue = findBlue();
-
- verify(enterCallback).onSharedElementEnd(names.capture(), views.capture(),
- snapshots.capture());
- assertEquals(1, names.getValue().size());
- assertEquals(1, views.getValue().size());
- assertNull(snapshots.getValue());
- assertEquals("blueSquare", names.getValue().get(0));
- assertEquals(reenterBlue, views.getValue().get(0));
- }
-
- // Make sure that onMapSharedElement works to change the shared element going out
- @Test
- public void onMapSharedElementOut() throws Throwable {
- final TransitionFragment fragment1 = setupInitialFragment();
-
- // Now do a transition to scene2
- TransitionFragment fragment2 = new TransitionFragment();
- fragment2.setLayoutId(R.layout.scene2);
-
- final View startBlue = findBlue();
- final View startGreen = findGreen();
-
- final Rect startGreenBounds = getBoundsOnScreen(startGreen);
-
- SharedElementCallback mapOut = new SharedElementCallback() {
- @Override
- public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
- assertEquals(1, names.size());
- assertEquals("blueSquare", names.get(0));
- assertEquals(1, sharedElements.size());
- assertEquals(startBlue, sharedElements.get("blueSquare"));
- sharedElements.put("blueSquare", startGreen);
- }
- };
- fragment1.setExitSharedElementCallback(mapOut);
-
- mFragmentManager.beginTransaction()
- .addSharedElement(startBlue, "blueSquare")
- .replace(R.id.fragmentContainer, fragment2)
- .setReorderingAllowed(mReorderingAllowed)
- .addToBackStack(null)
- .commit();
- FragmentTestUtil.waitForExecution(mActivityRule);
-
- fragment1.waitForTransition();
- fragment2.waitForTransition();
-
- final View endBlue = findBlue();
- final Rect endBlueBounds = getBoundsOnScreen(endBlue);
-
- verifyAndClearTransition(fragment2.sharedElementEnter, startGreenBounds, startGreen,
- endBlue);
-
- SharedElementCallback mapBack = new SharedElementCallback() {
- @Override
- public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
- assertEquals(1, names.size());
- assertEquals("blueSquare", names.get(0));
- assertEquals(1, sharedElements.size());
- final View expectedBlue = findViewById(fragment1, R.id.blueSquare);
- assertEquals(expectedBlue, sharedElements.get("blueSquare"));
- final View greenSquare = findViewById(fragment1, R.id.greenSquare);
- sharedElements.put("blueSquare", greenSquare);
- }
- };
- fragment1.setExitSharedElementCallback(mapBack);
-
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
-
- fragment1.waitForTransition();
- fragment2.waitForTransition();
-
- final View reenterGreen = findGreen();
- verifyAndClearTransition(fragment2.sharedElementReturn, endBlueBounds, endBlue,
- reenterGreen);
- }
-
- // Make sure that onMapSharedElement works to change the shared element target
- @Test
- public void onMapSharedElementIn() throws Throwable {
- TransitionFragment fragment1 = setupInitialFragment();
-
- // Now do a transition to scene2
- final TransitionFragment fragment2 = new TransitionFragment();
- fragment2.setLayoutId(R.layout.scene2);
-
- final View startBlue = findBlue();
- final Rect startBlueBounds = getBoundsOnScreen(startBlue);
-
- SharedElementCallback mapIn = new SharedElementCallback() {
- @Override
- public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
- assertEquals(1, names.size());
- assertEquals("blueSquare", names.get(0));
- assertEquals(1, sharedElements.size());
- final View blueSquare = findViewById(fragment2, R.id.blueSquare);
- assertEquals(blueSquare, sharedElements.get("blueSquare"));
- final View greenSquare = findViewById(fragment2, R.id.greenSquare);
- sharedElements.put("blueSquare", greenSquare);
- }
- };
- fragment2.setEnterSharedElementCallback(mapIn);
-
- mFragmentManager.beginTransaction()
- .addSharedElement(startBlue, "blueSquare")
- .replace(R.id.fragmentContainer, fragment2)
- .setReorderingAllowed(mReorderingAllowed)
- .addToBackStack(null)
- .commit();
- FragmentTestUtil.waitForExecution(mActivityRule);
-
- fragment1.waitForTransition();
- fragment2.waitForTransition();
-
- final View endGreen = findGreen();
- final View endBlue = findBlue();
- final Rect endGreenBounds = getBoundsOnScreen(endGreen);
-
- verifyAndClearTransition(fragment2.sharedElementEnter, startBlueBounds, startBlue,
- endGreen);
-
- SharedElementCallback mapBack = new SharedElementCallback() {
- @Override
- public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
- assertEquals(1, names.size());
- assertEquals("blueSquare", names.get(0));
- assertEquals(1, sharedElements.size());
- assertEquals(endBlue, sharedElements.get("blueSquare"));
- sharedElements.put("blueSquare", endGreen);
- }
- };
- fragment2.setEnterSharedElementCallback(mapBack);
-
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
-
- fragment1.waitForTransition();
- fragment2.waitForTransition();
-
- final View reenterBlue = findBlue();
- verifyAndClearTransition(fragment2.sharedElementReturn, endGreenBounds, endGreen,
- reenterBlue);
- }
-
- // Ensure that shared element transitions that have targets properly target the views
- @Test
- public void complexSharedElementTransition() throws Throwable {
- TransitionFragment fragment1 = setupInitialFragment();
-
- // Now do a transition to scene2
- ComplexTransitionFragment fragment2 = new ComplexTransitionFragment();
- fragment2.setLayoutId(R.layout.scene2);
-
- final View startBlue = findBlue();
- final View startGreen = findGreen();
- final Rect startBlueBounds = getBoundsOnScreen(startBlue);
-
- mFragmentManager.beginTransaction()
- .addSharedElement(startBlue, "blueSquare")
- .addSharedElement(startGreen, "greenSquare")
- .replace(R.id.fragmentContainer, fragment2)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- FragmentTestUtil.waitForExecution(mActivityRule);
- assertEquals(2, mOnBackStackChangedTimes);
-
- fragment1.waitForTransition();
- fragment2.waitForTransition();
-
- final View endBlue = findBlue();
- final View endGreen = findGreen();
- final Rect endBlueBounds = getBoundsOnScreen(endBlue);
-
- verifyAndClearTransition(fragment2.sharedElementEnterTransition1, startBlueBounds,
- startBlue, endBlue);
- verifyAndClearTransition(fragment2.sharedElementEnterTransition2, startBlueBounds,
- startGreen, endGreen);
-
- // Now see if it works when popped
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
- assertEquals(3, mOnBackStackChangedTimes);
-
- fragment1.waitForTransition();
- fragment2.waitForTransition();
-
- final View reenterBlue = findBlue();
- final View reenterGreen = findGreen();
-
- verifyAndClearTransition(fragment2.sharedElementReturnTransition1, endBlueBounds,
- endBlue, reenterBlue);
- verifyAndClearTransition(fragment2.sharedElementReturnTransition2, endBlueBounds,
- endGreen, reenterGreen);
- }
-
- // Ensure that after transitions have executed that they don't have any targets or other
- // unfortunate modifications.
- @Test
- public void transitionsEndUnchanged() throws Throwable {
- TransitionFragment fragment1 = setupInitialFragment();
-
- // Now do a transition to scene2
- TransitionFragment fragment2 = new TransitionFragment();
- fragment2.setLayoutId(R.layout.scene2);
-
- verifyTransition(fragment1, fragment2, "blueSquare");
- assertEquals(0, fragment1.exitTransition.getTargets().size());
- assertEquals(0, fragment2.sharedElementEnter.getTargets().size());
- assertEquals(0, fragment2.enterTransition.getTargets().size());
- assertNull(fragment1.exitTransition.getEpicenterCallback());
- assertNull(fragment2.enterTransition.getEpicenterCallback());
- assertNull(fragment2.sharedElementEnter.getEpicenterCallback());
-
- // Now pop the back stack
- verifyPopTransition(1, fragment2, fragment1);
-
- assertEquals(0, fragment2.returnTransition.getTargets().size());
- assertEquals(0, fragment2.sharedElementReturn.getTargets().size());
- assertEquals(0, fragment1.reenterTransition.getTargets().size());
- assertNull(fragment2.returnTransition.getEpicenterCallback());
- assertNull(fragment2.sharedElementReturn.getEpicenterCallback());
- assertNull(fragment2.reenterTransition.getEpicenterCallback());
- }
-
- // Ensure that transitions are done when a fragment is shown and hidden
- @Test
- public void showHideTransition() throws Throwable {
- TransitionFragment fragment1 = setupInitialFragment();
- TransitionFragment fragment2 = new TransitionFragment();
- fragment2.setLayoutId(R.layout.scene2);
-
- final View startBlue = findBlue();
- final View startGreen = findGreen();
-
- mFragmentManager.beginTransaction()
- .setReorderingAllowed(mReorderingAllowed)
- .add(R.id.fragmentContainer, fragment2)
- .hide(fragment1)
- .addToBackStack(null)
- .commit();
-
- FragmentTestUtil.waitForExecution(mActivityRule);
- fragment1.waitForTransition();
- fragment2.waitForTransition();
-
- final View endGreen = findViewById(fragment2, R.id.greenSquare);
- final View endBlue = findViewById(fragment2, R.id.blueSquare);
-
- assertEquals(View.GONE, fragment1.requireView().getVisibility());
- assertEquals(View.VISIBLE, startGreen.getVisibility());
- assertEquals(View.VISIBLE, startBlue.getVisibility());
-
- verifyAndClearTransition(fragment1.exitTransition, null, startGreen, startBlue);
- verifyNoOtherTransitions(fragment1);
-
- verifyAndClearTransition(fragment2.enterTransition, null, endGreen, endBlue);
- verifyNoOtherTransitions(fragment2);
-
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
-
- FragmentTestUtil.waitForExecution(mActivityRule);
- fragment1.waitForTransition();
- fragment2.waitForTransition();
-
- verifyAndClearTransition(fragment1.reenterTransition, null, startGreen, startBlue);
- verifyNoOtherTransitions(fragment1);
-
- assertEquals(View.VISIBLE, fragment1.requireView().getVisibility());
- assertEquals(View.VISIBLE, startGreen.getVisibility());
- assertEquals(View.VISIBLE, startBlue.getVisibility());
-
- verifyAndClearTransition(fragment2.returnTransition, null, endGreen, endBlue);
- verifyNoOtherTransitions(fragment2);
- }
-
- // Ensure that transitions are done when a fragment is attached and detached
- @Test
- public void attachDetachTransition() throws Throwable {
- TransitionFragment fragment1 = setupInitialFragment();
- TransitionFragment fragment2 = new TransitionFragment();
- fragment2.setLayoutId(R.layout.scene2);
-
- final View startBlue = findBlue();
- final View startGreen = findGreen();
-
- mFragmentManager.beginTransaction()
- .setReorderingAllowed(mReorderingAllowed)
- .add(R.id.fragmentContainer, fragment2)
- .detach(fragment1)
- .addToBackStack(null)
- .commit();
-
- FragmentTestUtil.waitForExecution(mActivityRule);
-
- final View endGreen = findViewById(fragment2, R.id.greenSquare);
- final View endBlue = findViewById(fragment2, R.id.blueSquare);
-
- verifyAndClearTransition(fragment1.exitTransition, null, startGreen, startBlue);
- verifyNoOtherTransitions(fragment1);
-
- verifyAndClearTransition(fragment2.enterTransition, null, endGreen, endBlue);
- verifyNoOtherTransitions(fragment2);
-
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
-
- FragmentTestUtil.waitForExecution(mActivityRule);
-
- final View reenterBlue = findBlue();
- final View reenterGreen = findGreen();
-
- verifyAndClearTransition(fragment1.reenterTransition, null, reenterGreen, reenterBlue);
- verifyNoOtherTransitions(fragment1);
-
- verifyAndClearTransition(fragment2.returnTransition, null, endGreen, endBlue);
- verifyNoOtherTransitions(fragment2);
- }
-
- // Ensure that shared element without matching transition name doesn't error out
- @Test
- public void sharedElementMismatch() throws Throwable {
- final TransitionFragment fragment1 = setupInitialFragment();
-
- // Now do a transition to scene2
- TransitionFragment fragment2 = new TransitionFragment();
- fragment2.setLayoutId(R.layout.scene2);
-
- final View startBlue = findBlue();
- final View startGreen = findGreen();
- final Rect startBlueBounds = getBoundsOnScreen(startBlue);
-
- mFragmentManager.beginTransaction()
- .addSharedElement(startBlue, "fooSquare")
- .replace(R.id.fragmentContainer, fragment2)
- .setReorderingAllowed(mReorderingAllowed)
- .addToBackStack(null)
- .commit();
- FragmentTestUtil.waitForExecution(mActivityRule);
-
- fragment1.waitForTransition();
- fragment2.waitForTransition();
-
- final View endBlue = findBlue();
- final View endGreen = findGreen();
-
- if (mReorderingAllowed) {
- verifyAndClearTransition(fragment1.exitTransition, null, startGreen, startBlue);
- } else {
- verifyAndClearTransition(fragment1.exitTransition, startBlueBounds, startGreen);
- verifyAndClearTransition(fragment2.sharedElementEnter, startBlueBounds, startBlue);
- }
- verifyNoOtherTransitions(fragment1);
-
- verifyAndClearTransition(fragment2.enterTransition, null, endGreen, endBlue);
- verifyNoOtherTransitions(fragment2);
- }
-
- // Ensure that using the same source or target shared element results in an exception.
- @Test
- public void sharedDuplicateTargetNames() throws Throwable {
- setupInitialFragment();
-
- final View startBlue = findBlue();
- final View startGreen = findGreen();
-
- FragmentTransaction ft = mFragmentManager.beginTransaction();
- ft.addSharedElement(startBlue, "blueSquare");
- try {
- ft.addSharedElement(startGreen, "blueSquare");
- fail("Expected IllegalArgumentException");
- } catch (IllegalArgumentException e) {
- // expected
- }
-
- try {
- ft.addSharedElement(startBlue, "greenSquare");
- fail("Expected IllegalArgumentException");
- } catch (IllegalArgumentException e) {
- // expected
- }
- }
-
- // Test that invisible fragment views don't participate in transitions
- @Test
- public void invisibleNoTransitions() throws Throwable {
- if (!mReorderingAllowed) {
- return; // only reordered transitions can avoid interaction
- }
- // enter transition
- TransitionFragment fragment = new InvisibleFragment();
- fragment.setLayoutId(R.layout.scene1);
- mFragmentManager.beginTransaction()
- .setReorderingAllowed(mReorderingAllowed)
- .add(R.id.fragmentContainer, fragment)
- .addToBackStack(null)
- .commit();
- FragmentTestUtil.waitForExecution(mActivityRule);
- fragment.waitForNoTransition();
- verifyNoOtherTransitions(fragment);
-
- // exit transition
- mFragmentManager.beginTransaction()
- .setReorderingAllowed(mReorderingAllowed)
- .remove(fragment)
- .addToBackStack(null)
- .commit();
-
- fragment.waitForNoTransition();
- verifyNoOtherTransitions(fragment);
-
- // reenter transition
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
- fragment.waitForNoTransition();
- verifyNoOtherTransitions(fragment);
-
- // return transition
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
- fragment.waitForNoTransition();
- verifyNoOtherTransitions(fragment);
- }
-
- // No crash when transitioning a shared element and there is no shared element transition.
- @Test
- public void noSharedElementTransition() throws Throwable {
- TransitionFragment fragment1 = setupInitialFragment();
-
- final View startBlue = findBlue();
- final View startGreen = findGreen();
- final Rect startBlueBounds = getBoundsOnScreen(startBlue);
-
- TransitionFragment fragment2 = new TransitionFragment();
- fragment2.setLayoutId(R.layout.scene2);
-
- mFragmentManager.beginTransaction()
- .setReorderingAllowed(mReorderingAllowed)
- .addSharedElement(startBlue, "blueSquare")
- .replace(R.id.fragmentContainer, fragment2)
- .addToBackStack(null)
- .commit();
-
- fragment1.waitForTransition();
- fragment2.waitForTransition();
- final View midGreen = findGreen();
- final View midBlue = findBlue();
- final Rect midBlueBounds = getBoundsOnScreen(midBlue);
- verifyAndClearTransition(fragment1.exitTransition, startBlueBounds, startGreen);
- verifyAndClearTransition(fragment2.sharedElementEnter, startBlueBounds, startBlue, midBlue);
- verifyAndClearTransition(fragment2.enterTransition, midBlueBounds, midGreen);
- verifyNoOtherTransitions(fragment1);
- verifyNoOtherTransitions(fragment2);
-
- final TransitionFragment fragment3 = new TransitionFragment();
- fragment3.setLayoutId(R.layout.scene3);
-
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- fm.popBackStack();
- fm.beginTransaction()
- .setReorderingAllowed(mReorderingAllowed)
- .replace(R.id.fragmentContainer, fragment3)
- .addToBackStack(null)
- .commit();
- }
- });
-
- // This shouldn't give an error.
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- fragment2.waitForTransition();
- // It does not transition properly for ordered transactions, though.
- if (mReorderingAllowed) {
- verifyAndClearTransition(fragment2.returnTransition, null, midGreen, midBlue);
- final View endGreen = findGreen();
- final View endBlue = findBlue();
- final View endRed = findRed();
- verifyAndClearTransition(fragment3.enterTransition, null, endGreen, endBlue, endRed);
- verifyNoOtherTransitions(fragment2);
- verifyNoOtherTransitions(fragment3);
- } else {
- // fragment3 doesn't get a transition since it conflicts with the pop transition
- verifyNoOtherTransitions(fragment3);
- // Everything else is just doing its best. Ordered transactions can't handle
- // multiple transitions acting together except for popping multiple together.
- }
- }
-
- // When there is no matching shared element, the transition name should not be changed
- @Test
- public void noMatchingSharedElementRetainName() throws Throwable {
- TransitionFragment fragment1 = setupInitialFragment();
-
- final View startBlue = findBlue();
- final View startGreen = findGreen();
- final Rect startGreenBounds = getBoundsOnScreen(startGreen);
-
- TransitionFragment fragment2 = new TransitionFragment();
- fragment2.setLayoutId(R.layout.scene3);
-
- mFragmentManager.beginTransaction()
- .setReorderingAllowed(mReorderingAllowed)
- .addSharedElement(startGreen, "greenSquare")
- .addSharedElement(startBlue, "blueSquare")
- .replace(R.id.fragmentContainer, fragment2)
- .addToBackStack(null)
- .commit();
-
- fragment2.waitForTransition();
- final View midGreen = findGreen();
- final View midBlue = findBlue();
- final View midRed = findRed();
- final Rect midGreenBounds = getBoundsOnScreen(midGreen);
- if (mReorderingAllowed) {
- verifyAndClearTransition(fragment2.sharedElementEnter, startGreenBounds, startGreen,
- midGreen);
- } else {
- verifyAndClearTransition(fragment2.sharedElementEnter, startGreenBounds, startGreen,
- midGreen, startBlue);
- }
- verifyAndClearTransition(fragment2.enterTransition, midGreenBounds, midBlue, midRed);
- verifyNoOtherTransitions(fragment2);
-
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
- fragment2.waitForTransition();
- fragment1.waitForTransition();
-
- final View endBlue = findBlue();
- final View endGreen = findGreen();
-
- assertEquals("blueSquare", endBlue.getTransitionName());
- assertEquals("greenSquare", endGreen.getTransitionName());
- }
-
- private TransitionFragment setupInitialFragment() throws Throwable {
- TransitionFragment fragment1 = new TransitionFragment();
- fragment1.setLayoutId(R.layout.scene1);
- mFragmentManager.beginTransaction()
- .setReorderingAllowed(mReorderingAllowed)
- .add(R.id.fragmentContainer, fragment1)
- .addToBackStack(null)
- .commit();
- FragmentTestUtil.waitForExecution(mActivityRule);
- assertEquals(1, mOnBackStackChangedTimes);
- fragment1.waitForTransition();
- final View blueSquare1 = findBlue();
- final View greenSquare1 = findGreen();
- verifyAndClearTransition(fragment1.enterTransition, null, blueSquare1, greenSquare1);
- verifyNoOtherTransitions(fragment1);
- return fragment1;
- }
-
- private View findViewById(Fragment fragment, int id) {
- return fragment.requireView().findViewById(id);
- }
-
- private View findGreen() {
- return mActivityRule.getActivity().findViewById(R.id.greenSquare);
- }
-
- private View findBlue() {
- return mActivityRule.getActivity().findViewById(R.id.blueSquare);
- }
-
- private View findRed() {
- return mActivityRule.getActivity().findViewById(R.id.redSquare);
- }
-
- private void verifyAndClearTransition(TargetTracking transition, Rect epicenter,
- View... expected) {
- if (epicenter == null) {
- assertNull(transition.getCapturedEpicenter());
- } else {
- assertEquals(epicenter, transition.getCapturedEpicenter());
- }
- ArrayList<View> targets = transition.getTrackedTargets();
- StringBuilder sb = new StringBuilder();
- sb
- .append("Expected: [")
- .append(expected.length)
- .append("] {");
- boolean isFirst = true;
- for (View view : expected) {
- if (isFirst) {
- isFirst = false;
- } else {
- sb.append(", ");
- }
- sb.append(view);
- }
- sb
- .append("}, but got: [")
- .append(targets.size())
- .append("] {");
- isFirst = true;
- for (View view : targets) {
- if (isFirst) {
- isFirst = false;
- } else {
- sb.append(", ");
- }
- sb.append(view);
- }
- sb.append("}");
- String errorMessage = sb.toString();
-
- assertEquals(errorMessage, expected.length, targets.size());
- for (View view : expected) {
- assertTrue(errorMessage, targets.contains(view));
- }
- transition.clearTargets();
- }
-
- private void verifyNoOtherTransitions(TransitionFragment fragment) {
- assertEquals(0, fragment.enterTransition.targets.size());
- assertEquals(0, fragment.exitTransition.targets.size());
- assertEquals(0, fragment.reenterTransition.targets.size());
- assertEquals(0, fragment.returnTransition.targets.size());
- assertEquals(0, fragment.sharedElementEnter.targets.size());
- assertEquals(0, fragment.sharedElementReturn.targets.size());
- }
-
- private void verifyTransition(TransitionFragment from, TransitionFragment to,
- String sharedElementName) throws Throwable {
- final int startOnBackStackChanged = mOnBackStackChangedTimes;
- final View startBlue = findBlue();
- final View startGreen = findGreen();
- final View startRed = findRed();
-
- final Rect startBlueRect = getBoundsOnScreen(startBlue);
-
- mFragmentManager.beginTransaction()
- .setReorderingAllowed(mReorderingAllowed)
- .addSharedElement(startBlue, sharedElementName)
- .replace(R.id.fragmentContainer, to)
- .addToBackStack(null)
- .commit();
-
- FragmentTestUtil.waitForExecution(mActivityRule);
- assertEquals(startOnBackStackChanged + 1, mOnBackStackChangedTimes);
-
- to.waitForTransition();
- final View endGreen = findGreen();
- final View endBlue = findBlue();
- final View endRed = findRed();
- final Rect endBlueRect = getBoundsOnScreen(endBlue);
-
- if (startRed != null) {
- verifyAndClearTransition(from.exitTransition, startBlueRect, startGreen, startRed);
- } else {
- verifyAndClearTransition(from.exitTransition, startBlueRect, startGreen);
- }
- verifyNoOtherTransitions(from);
-
- if (endRed != null) {
- verifyAndClearTransition(to.enterTransition, endBlueRect, endGreen, endRed);
- } else {
- verifyAndClearTransition(to.enterTransition, endBlueRect, endGreen);
- }
- verifyAndClearTransition(to.sharedElementEnter, startBlueRect, startBlue, endBlue);
- verifyNoOtherTransitions(to);
- }
-
- private void verifyCrossTransition(boolean swapSource,
- TransitionFragment from1, TransitionFragment from2) throws Throwable {
- final int startNumOnBackStackChanged = mOnBackStackChangedTimes;
- final int changesPerOperation = mReorderingAllowed ? 1 : 2;
-
- final TransitionFragment to1 = new TransitionFragment();
- to1.setLayoutId(R.layout.scene2);
- final TransitionFragment to2 = new TransitionFragment();
- to2.setLayoutId(R.layout.scene2);
-
- final View fromExit1 = findViewById(from1, R.id.greenSquare);
- final View fromShared1 = findViewById(from1, R.id.blueSquare);
- final Rect fromSharedRect1 = getBoundsOnScreen(fromShared1);
-
- final int fromExitId2 = swapSource ? R.id.blueSquare : R.id.greenSquare;
- final int fromSharedId2 = swapSource ? R.id.greenSquare : R.id.blueSquare;
- final View fromExit2 = findViewById(from2, fromExitId2);
- final View fromShared2 = findViewById(from2, fromSharedId2);
- final Rect fromSharedRect2 = getBoundsOnScreen(fromShared2);
-
- final String sharedElementName = swapSource ? "blueSquare" : "greenSquare";
-
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mFragmentManager.beginTransaction()
- .setReorderingAllowed(mReorderingAllowed)
- .addSharedElement(fromShared1, "blueSquare")
- .replace(R.id.fragmentContainer1, to1)
- .addToBackStack(null)
- .commit();
- mFragmentManager.beginTransaction()
- .setReorderingAllowed(mReorderingAllowed)
- .addSharedElement(fromShared2, sharedElementName)
- .replace(R.id.fragmentContainer2, to2)
- .addToBackStack(null)
- .commit();
- }
- });
- FragmentTestUtil.waitForExecution(mActivityRule);
- assertEquals(startNumOnBackStackChanged + changesPerOperation, mOnBackStackChangedTimes);
-
- from1.waitForTransition();
- from2.waitForTransition();
- to1.waitForTransition();
- to2.waitForTransition();
-
- final View toEnter1 = findViewById(to1, R.id.greenSquare);
- final View toShared1 = findViewById(to1, R.id.blueSquare);
- final Rect toSharedRect1 = getBoundsOnScreen(toShared1);
-
- final View toEnter2 = findViewById(to2, fromSharedId2);
- final View toShared2 = findViewById(to2, fromExitId2);
- final Rect toSharedRect2 = getBoundsOnScreen(toShared2);
-
- verifyAndClearTransition(from1.exitTransition, fromSharedRect1, fromExit1);
- verifyAndClearTransition(from2.exitTransition, fromSharedRect2, fromExit2);
- verifyNoOtherTransitions(from1);
- verifyNoOtherTransitions(from2);
-
- verifyAndClearTransition(to1.enterTransition, toSharedRect1, toEnter1);
- verifyAndClearTransition(to2.enterTransition, toSharedRect2, toEnter2);
- verifyAndClearTransition(to1.sharedElementEnter, fromSharedRect1, fromShared1, toShared1);
- verifyAndClearTransition(to2.sharedElementEnter, fromSharedRect2, fromShared2, toShared2);
- verifyNoOtherTransitions(to1);
- verifyNoOtherTransitions(to2);
-
- // Now pop it back
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mFragmentManager.popBackStack();
- mFragmentManager.popBackStack();
- }
- });
- FragmentTestUtil.waitForExecution(mActivityRule);
- assertEquals(startNumOnBackStackChanged + changesPerOperation + 1,
- mOnBackStackChangedTimes);
-
- from1.waitForTransition();
- from2.waitForTransition();
- to1.waitForTransition();
- to2.waitForTransition();
-
- final View returnEnter1 = findViewById(from1, R.id.greenSquare);
- final View returnShared1 = findViewById(from1, R.id.blueSquare);
-
- final View returnEnter2 = findViewById(from2, fromExitId2);
- final View returnShared2 = findViewById(from2, fromSharedId2);
-
- verifyAndClearTransition(to1.returnTransition, toSharedRect1, toEnter1);
- verifyAndClearTransition(to2.returnTransition, toSharedRect2, toEnter2);
- verifyAndClearTransition(to1.sharedElementReturn, toSharedRect1, toShared1, returnShared1);
- verifyAndClearTransition(to2.sharedElementReturn, toSharedRect2, toShared2, returnShared2);
- verifyNoOtherTransitions(to1);
- verifyNoOtherTransitions(to2);
-
- verifyAndClearTransition(from1.reenterTransition, fromSharedRect1, returnEnter1);
- verifyAndClearTransition(from2.reenterTransition, fromSharedRect2, returnEnter2);
- verifyNoOtherTransitions(from1);
- verifyNoOtherTransitions(from2);
- }
-
- private void verifyPopTransition(final int numPops, TransitionFragment from,
- TransitionFragment to, TransitionFragment... others) throws Throwable {
- final int startOnBackStackChanged = mOnBackStackChangedTimes;
- final View startBlue = findBlue();
- final View startGreen = findGreen();
- final View startRed = findRed();
- final Rect startSharedRect = getBoundsOnScreen(startBlue);
-
- mInstrumentation.runOnMainSync(new Runnable() {
- @Override
- public void run() {
- for (int i = 0; i < numPops; i++) {
- mFragmentManager.popBackStack();
- }
- }
- });
- FragmentTestUtil.waitForExecution(mActivityRule);
- assertEquals(startOnBackStackChanged + 1, mOnBackStackChangedTimes);
-
- to.waitForTransition();
- final View endGreen = findGreen();
- final View endBlue = findBlue();
- final View endRed = findRed();
- final Rect endSharedRect = getBoundsOnScreen(endBlue);
-
- if (startRed != null) {
- verifyAndClearTransition(from.returnTransition, startSharedRect, startGreen, startRed);
- } else {
- verifyAndClearTransition(from.returnTransition, startSharedRect, startGreen);
- }
- verifyAndClearTransition(from.sharedElementReturn, startSharedRect, startBlue, endBlue);
- verifyNoOtherTransitions(from);
-
- if (endRed != null) {
- verifyAndClearTransition(to.reenterTransition, endSharedRect, endGreen, endRed);
- } else {
- verifyAndClearTransition(to.reenterTransition, endSharedRect, endGreen);
- }
- verifyNoOtherTransitions(to);
-
- if (others != null) {
- for (TransitionFragment fragment : others) {
- verifyNoOtherTransitions(fragment);
- }
- }
- }
-
- private static Rect getBoundsOnScreen(View view) {
- final int[] loc = new int[2];
- view.getLocationOnScreen(loc);
- return new Rect(loc[0], loc[1], loc[0] + view.getWidth(), loc[1] + view.getHeight());
- }
-
- public static class ComplexTransitionFragment extends TransitionFragment {
- public final TrackingTransition sharedElementEnterTransition1 = new TrackingTransition();
- public final TrackingTransition sharedElementEnterTransition2 = new TrackingTransition();
- public final TrackingTransition sharedElementReturnTransition1 = new TrackingTransition();
- public final TrackingTransition sharedElementReturnTransition2 = new TrackingTransition();
-
- public final TransitionSet sharedElementEnterTransition = new TransitionSet()
- .addTransition(sharedElementEnterTransition1)
- .addTransition(sharedElementEnterTransition2);
- public final TransitionSet sharedElementReturnTransition = new TransitionSet()
- .addTransition(sharedElementReturnTransition1)
- .addTransition(sharedElementReturnTransition2);
-
- public ComplexTransitionFragment() {
- sharedElementEnterTransition1.addTarget(R.id.blueSquare);
- sharedElementEnterTransition2.addTarget(R.id.greenSquare);
- sharedElementReturnTransition1.addTarget(R.id.blueSquare);
- sharedElementReturnTransition2.addTarget(R.id.greenSquare);
- setSharedElementEnterTransition(sharedElementEnterTransition);
- setSharedElementReturnTransition(sharedElementReturnTransition);
- }
- }
-
- public static class InvisibleFragment extends TransitionFragment {
- @Override
- public void onViewCreated(View view, Bundle savedInstanceState) {
- view.setVisibility(View.INVISIBLE);
- super.onViewCreated(view, savedInstanceState);
- }
- }
-}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionTest.kt
new file mode 100644
index 0000000..83d8465
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionTest.kt
@@ -0,0 +1,1147 @@
+/*
+ * 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 androidx.fragment.app
+
+import android.graphics.Rect
+import android.os.Build
+import android.os.Bundle
+import android.transition.TransitionSet
+import android.view.View
+import androidx.core.app.SharedElementCallback
+import androidx.fragment.app.test.FragmentTestActivity
+import androidx.fragment.test.R
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.ActivityTestRule
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.After
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+
+@MediumTest
+@RunWith(Parameterized::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+class FragmentTransitionTest(private val reorderingAllowed: Boolean) {
+
+ @get:Rule
+ val activityRule = ActivityTestRule(FragmentTestActivity::class.java)
+
+ private val instrumentation = InstrumentationRegistry.getInstrumentation()
+ private lateinit var fragmentManager: FragmentManager
+ private var onBackStackChangedTimes: Int = 0
+ private val onBackStackChangedListener =
+ FragmentManager.OnBackStackChangedListener { onBackStackChangedTimes++ }
+
+ @Before
+ fun setup() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ onBackStackChangedTimes = 0
+ fragmentManager = activityRule.activity.supportFragmentManager
+ fragmentManager.addOnBackStackChangedListener(onBackStackChangedListener)
+ }
+
+ @After
+ fun teardown() {
+ fragmentManager.removeOnBackStackChangedListener(onBackStackChangedListener)
+ }
+
+ // Test that normal view transitions (enter, exit, reenter, return) run with
+ // a single fragment.
+ @Test
+ fun enterExitTransitions() {
+ // enter transition
+ val fragment = setupInitialFragment()
+ val blue = findBlue()
+ val green = findBlue()
+
+ // exit transition
+ fragmentManager.beginTransaction()
+ .setReorderingAllowed(reorderingAllowed)
+ .remove(fragment)
+ .addToBackStack(null)
+ .commit()
+
+ fragment.waitForTransition()
+ verifyAndClearTransition(fragment.exitTransition, null, green, blue)
+ verifyNoOtherTransitions(fragment)
+ assertThat(onBackStackChangedTimes).isEqualTo(2)
+
+ // reenter transition
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+ fragment.waitForTransition()
+ val green2 = findGreen()
+ val blue2 = findBlue()
+ verifyAndClearTransition(fragment.reenterTransition, null, green2, blue2)
+ verifyNoOtherTransitions(fragment)
+ assertThat(onBackStackChangedTimes).isEqualTo(3)
+
+ // return transition
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+ fragment.waitForTransition()
+ verifyAndClearTransition(fragment.returnTransition, null, green2, blue2)
+ verifyNoOtherTransitions(fragment)
+ assertThat(onBackStackChangedTimes).isEqualTo(4)
+ }
+
+ // Test that shared elements transition from one fragment to the next
+ // and back during pop.
+ @Test
+ fun sharedElement() {
+ val fragment1 = setupInitialFragment()
+
+ // Now do a transition to scene2
+ val fragment2 = TransitionFragment(R.layout.scene2)
+
+ verifyTransition(fragment1, fragment2, "blueSquare")
+
+ // Now pop the back stack
+ verifyPopTransition(1, fragment2, fragment1)
+ }
+
+ // Test that shared element transitions through multiple fragments work together
+ @Test
+ fun intermediateFragment() {
+ val fragment1 = setupInitialFragment()
+
+ val fragment2 = TransitionFragment(R.layout.scene3)
+
+ verifyTransition(fragment1, fragment2, "shared")
+
+ val fragment3 = TransitionFragment(R.layout.scene2)
+
+ verifyTransition(fragment2, fragment3, "blueSquare")
+
+ // Should transfer backwards when popping multiple:
+ verifyPopTransition(2, fragment3, fragment1, fragment2)
+ }
+
+ // Adding/removing the same fragment multiple times shouldn't mess anything up
+ @Test
+ fun removeAdded() {
+ val fragment1 = setupInitialFragment()
+
+ val startBlue = findBlue()
+ val startGreen = findGreen()
+
+ val fragment2 = TransitionFragment(R.layout.scene2)
+
+ instrumentation.runOnMainSync {
+ fragmentManager.beginTransaction()
+ .setReorderingAllowed(reorderingAllowed)
+ .replace(R.id.fragmentContainer, fragment2)
+ .replace(R.id.fragmentContainer, fragment1)
+ .replace(R.id.fragmentContainer, fragment2)
+ .addToBackStack(null)
+ .commit()
+ }
+ FragmentTestUtil.waitForExecution(activityRule)
+ assertThat(onBackStackChangedTimes).isEqualTo(2)
+
+ // should be a normal transition from fragment1 to fragment2
+ fragment2.waitForTransition()
+ val endBlue = findBlue()
+ val endGreen = findGreen()
+ verifyAndClearTransition(fragment1.exitTransition, null, startBlue, startGreen)
+ verifyAndClearTransition(fragment2.enterTransition, null, endBlue, endGreen)
+ verifyNoOtherTransitions(fragment1)
+ verifyNoOtherTransitions(fragment2)
+
+ // Pop should also do the same thing
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+ assertThat(onBackStackChangedTimes).isEqualTo(3)
+
+ fragment1.waitForTransition()
+ val popBlue = findBlue()
+ val popGreen = findGreen()
+ verifyAndClearTransition(fragment1.reenterTransition, null, popBlue, popGreen)
+ verifyAndClearTransition(fragment2.returnTransition, null, endBlue, endGreen)
+ verifyNoOtherTransitions(fragment1)
+ verifyNoOtherTransitions(fragment2)
+ }
+
+ // Make sure that shared elements on two different fragment containers don't interact
+ @Test
+ fun crossContainer() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.double_container)
+ val fragment1 = TransitionFragment(R.layout.scene1)
+ val fragment2 = TransitionFragment(R.layout.scene1)
+ fragmentManager.beginTransaction()
+ .setReorderingAllowed(reorderingAllowed)
+ .add(R.id.fragmentContainer1, fragment1)
+ .add(R.id.fragmentContainer2, fragment2)
+ .addToBackStack(null)
+ .commit()
+ FragmentTestUtil.waitForExecution(activityRule)
+ assertThat(onBackStackChangedTimes).isEqualTo(1)
+
+ fragment1.waitForTransition()
+ val greenSquare1 = findViewById(fragment1, R.id.greenSquare)
+ val blueSquare1 = findViewById(fragment1, R.id.blueSquare)
+ verifyAndClearTransition(fragment1.enterTransition, null, greenSquare1, blueSquare1)
+ verifyNoOtherTransitions(fragment1)
+ fragment2.waitForTransition()
+ val greenSquare2 = findViewById(fragment2, R.id.greenSquare)
+ val blueSquare2 = findViewById(fragment2, R.id.blueSquare)
+ verifyAndClearTransition(fragment2.enterTransition, null, greenSquare2, blueSquare2)
+ verifyNoOtherTransitions(fragment2)
+
+ // Make sure the correct transitions are run when the target names
+ // are different in both shared elements. We may fool the system.
+ verifyCrossTransition(false, fragment1, fragment2)
+
+ // Make sure the correct transitions are run when the source names
+ // are different in both shared elements. We may fool the system.
+ verifyCrossTransition(true, fragment1, fragment2)
+ }
+
+ // Make sure that onSharedElementStart and onSharedElementEnd are called
+ @Test
+ fun callStartEndWithSharedElements() {
+ val fragment1 = setupInitialFragment()
+
+ // Now do a transition to scene2
+ val fragment2 = TransitionFragment(R.layout.scene2)
+
+ val enterCallback = mock(SharedElementCallback::class.java)
+ fragment2.setEnterSharedElementCallback(enterCallback)
+
+ val startBlue = findBlue()
+
+ verifyTransition(fragment1, fragment2, "blueSquare")
+
+ val names = ArgumentCaptor.forClass(List::class.java as Class<List<String>>)
+ val views = ArgumentCaptor.forClass(List::class.java as Class<List<View>>)
+ val snapshots = ArgumentCaptor.forClass(List::class.java as Class<List<View>>)
+ verify(enterCallback).onSharedElementStart(
+ names.capture(), views.capture(),
+ snapshots.capture()
+ )
+ assertThat(names.value.size).isEqualTo(1)
+ assertThat(views.value.size).isEqualTo(1)
+ assertThat(snapshots.value).isNull()
+ assertThat(names.value[0]).isEqualTo("blueSquare")
+ assertThat(views.value[0]).isEqualTo(startBlue)
+
+ val endBlue = findBlue()
+
+ verify(enterCallback).onSharedElementEnd(
+ names.capture(), views.capture(),
+ snapshots.capture()
+ )
+ assertThat(names.value.size).isEqualTo(1)
+ assertThat(views.value.size).isEqualTo(1)
+ assertThat(snapshots.value).isNull()
+ assertThat(names.value[0]).isEqualTo("blueSquare")
+ assertThat(views.value[0]).isEqualTo(endBlue)
+
+ // Now pop the back stack
+ reset(enterCallback)
+ verifyPopTransition(1, fragment2, fragment1)
+
+ verify(enterCallback).onSharedElementStart(
+ names.capture(), views.capture(),
+ snapshots.capture()
+ )
+ assertThat(names.value.size).isEqualTo(1)
+ assertThat(views.value.size).isEqualTo(1)
+ assertThat(snapshots.value).isNull()
+ assertThat(names.value[0]).isEqualTo("blueSquare")
+ assertThat(views.value[0]).isEqualTo(endBlue)
+
+ val reenterBlue = findBlue()
+
+ verify(enterCallback).onSharedElementEnd(
+ names.capture(), views.capture(),
+ snapshots.capture()
+ )
+ assertThat(names.value.size).isEqualTo(1)
+ assertThat(views.value.size).isEqualTo(1)
+ assertThat(snapshots.value).isNull()
+ assertThat(names.value[0]).isEqualTo("blueSquare")
+ assertThat(views.value[0]).isEqualTo(reenterBlue)
+ }
+
+ // Make sure that onMapSharedElement works to change the shared element going out
+ @Test
+ fun onMapSharedElementOut() {
+ val fragment1 = setupInitialFragment()
+
+ // Now do a transition to scene2
+ val fragment2 = TransitionFragment(R.layout.scene2)
+
+ val startBlue = findBlue()
+ val startGreen = findGreen()
+
+ val startGreenBounds = getBoundsOnScreen(startGreen)
+
+ val mapOut = object : SharedElementCallback() {
+ override fun onMapSharedElements(
+ names: List<String>,
+ sharedElements: MutableMap<String, View>
+ ) {
+ assertThat(names.size).isEqualTo(1)
+ assertThat(names[0]).isEqualTo("blueSquare")
+ assertThat(sharedElements.size).isEqualTo(1)
+ assertThat(sharedElements["blueSquare"]).isEqualTo(startBlue)
+ sharedElements["blueSquare"] = startGreen
+ }
+ }
+ fragment1.setExitSharedElementCallback(mapOut)
+
+ fragmentManager.beginTransaction()
+ .addSharedElement(startBlue, "blueSquare")
+ .replace(R.id.fragmentContainer, fragment2)
+ .setReorderingAllowed(reorderingAllowed)
+ .addToBackStack(null)
+ .commit()
+ FragmentTestUtil.waitForExecution(activityRule)
+
+ fragment1.waitForTransition()
+ fragment2.waitForTransition()
+
+ val endBlue = findBlue()
+ val endBlueBounds = getBoundsOnScreen(endBlue)
+
+ verifyAndClearTransition(
+ fragment2.sharedElementEnter, startGreenBounds, startGreen,
+ endBlue
+ )
+
+ val mapBack = object : SharedElementCallback() {
+ override fun onMapSharedElements(
+ names: List<String>,
+ sharedElements: MutableMap<String, View>
+ ) {
+ assertThat(names.size).isEqualTo(1)
+ assertThat(names[0]).isEqualTo("blueSquare")
+ assertThat(sharedElements.size).isEqualTo(1)
+ val expectedBlue = findViewById(fragment1, R.id.blueSquare)
+ assertThat(sharedElements["blueSquare"]).isEqualTo(expectedBlue)
+ val greenSquare = findViewById(fragment1, R.id.greenSquare)
+ sharedElements["blueSquare"] = greenSquare
+ }
+ }
+ fragment1.setExitSharedElementCallback(mapBack)
+
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+
+ fragment1.waitForTransition()
+ fragment2.waitForTransition()
+
+ val reenterGreen = findGreen()
+ verifyAndClearTransition(
+ fragment2.sharedElementReturn, endBlueBounds, endBlue,
+ reenterGreen
+ )
+ }
+
+ // Make sure that onMapSharedElement works to change the shared element target
+ @Test
+ fun onMapSharedElementIn() {
+ val fragment1 = setupInitialFragment()
+
+ // Now do a transition to scene2
+ val fragment2 = TransitionFragment(R.layout.scene2)
+
+ val startBlue = findBlue()
+ val startBlueBounds = getBoundsOnScreen(startBlue)
+
+ val mapIn = object : SharedElementCallback() {
+ override fun onMapSharedElements(
+ names: List<String>,
+ sharedElements: MutableMap<String, View>
+ ) {
+ assertThat(names.size).isEqualTo(1)
+ assertThat(names[0]).isEqualTo("blueSquare")
+ assertThat(sharedElements.size).isEqualTo(1)
+ val blueSquare = findViewById(fragment2, R.id.blueSquare)
+ assertThat(sharedElements["blueSquare"]).isEqualTo(blueSquare)
+ val greenSquare = findViewById(fragment2, R.id.greenSquare)
+ sharedElements["blueSquare"] = greenSquare
+ }
+ }
+ fragment2.setEnterSharedElementCallback(mapIn)
+
+ fragmentManager.beginTransaction()
+ .addSharedElement(startBlue, "blueSquare")
+ .replace(R.id.fragmentContainer, fragment2)
+ .setReorderingAllowed(reorderingAllowed)
+ .addToBackStack(null)
+ .commit()
+ FragmentTestUtil.waitForExecution(activityRule)
+
+ fragment1.waitForTransition()
+ fragment2.waitForTransition()
+
+ val endGreen = findGreen()
+ val endBlue = findBlue()
+ val endGreenBounds = getBoundsOnScreen(endGreen)
+
+ verifyAndClearTransition(
+ fragment2.sharedElementEnter, startBlueBounds, startBlue,
+ endGreen
+ )
+
+ val mapBack = object : SharedElementCallback() {
+ override fun onMapSharedElements(
+ names: List<String>,
+ sharedElements: MutableMap<String, View>
+ ) {
+ assertThat(names.size).isEqualTo(1)
+ assertThat(names[0]).isEqualTo("blueSquare")
+ assertThat(sharedElements.size).isEqualTo(1)
+ assertThat(sharedElements["blueSquare"]).isEqualTo(endBlue)
+ sharedElements["blueSquare"] = endGreen
+ }
+ }
+ fragment2.setEnterSharedElementCallback(mapBack)
+
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+
+ fragment1.waitForTransition()
+ fragment2.waitForTransition()
+
+ val reenterBlue = findBlue()
+ verifyAndClearTransition(
+ fragment2.sharedElementReturn, endGreenBounds, endGreen,
+ reenterBlue
+ )
+ }
+
+ // Ensure that shared element transitions that have targets properly target the views
+ @Test
+ fun complexSharedElementTransition() {
+ val fragment1 = setupInitialFragment()
+
+ // Now do a transition to scene2
+ val fragment2 = ComplexTransitionFragment()
+
+ val startBlue = findBlue()
+ val startGreen = findGreen()
+ val startBlueBounds = getBoundsOnScreen(startBlue)
+
+ fragmentManager.beginTransaction()
+ .addSharedElement(startBlue, "blueSquare")
+ .addSharedElement(startGreen, "greenSquare")
+ .replace(R.id.fragmentContainer, fragment2)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ FragmentTestUtil.waitForExecution(activityRule)
+ assertThat(onBackStackChangedTimes).isEqualTo(2)
+
+ fragment1.waitForTransition()
+ fragment2.waitForTransition()
+
+ val endBlue = findBlue()
+ val endGreen = findGreen()
+ val endBlueBounds = getBoundsOnScreen(endBlue)
+
+ verifyAndClearTransition(
+ fragment2.sharedElementEnterTransition1, startBlueBounds,
+ startBlue, endBlue
+ )
+ verifyAndClearTransition(
+ fragment2.sharedElementEnterTransition2, startBlueBounds,
+ startGreen, endGreen
+ )
+
+ // Now see if it works when popped
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+ assertThat(onBackStackChangedTimes).isEqualTo(3)
+
+ fragment1.waitForTransition()
+ fragment2.waitForTransition()
+
+ val reenterBlue = findBlue()
+ val reenterGreen = findGreen()
+
+ verifyAndClearTransition(
+ fragment2.sharedElementReturnTransition1, endBlueBounds,
+ endBlue, reenterBlue
+ )
+ verifyAndClearTransition(
+ fragment2.sharedElementReturnTransition2, endBlueBounds,
+ endGreen, reenterGreen
+ )
+ }
+
+ // Ensure that after transitions have executed that they don't have any targets or other
+ // unfortunate modifications.
+ @Test
+ fun transitionsEndUnchanged() {
+ val fragment1 = setupInitialFragment()
+
+ // Now do a transition to scene2
+ val fragment2 = TransitionFragment(R.layout.scene2)
+
+ verifyTransition(fragment1, fragment2, "blueSquare")
+ assertThat(fragment1.exitTransition.getTargets().size).isEqualTo(0)
+ assertThat(fragment2.sharedElementEnter.getTargets().size).isEqualTo(0)
+ assertThat(fragment2.enterTransition.getTargets().size).isEqualTo(0)
+ assertThat(fragment1.exitTransition.epicenterCallback).isNull()
+ assertThat(fragment2.enterTransition.epicenterCallback).isNull()
+ assertThat(fragment2.sharedElementEnter.epicenterCallback).isNull()
+
+ // Now pop the back stack
+ verifyPopTransition(1, fragment2, fragment1)
+
+ assertThat(fragment2.returnTransition.getTargets().size).isEqualTo(0)
+ assertThat(fragment2.sharedElementReturn.getTargets().size).isEqualTo(0)
+ assertThat(fragment1.reenterTransition.getTargets().size).isEqualTo(0)
+ assertThat(fragment2.returnTransition.epicenterCallback).isNull()
+ assertThat(fragment2.sharedElementReturn.epicenterCallback).isNull()
+ assertThat(fragment2.reenterTransition.epicenterCallback).isNull()
+ }
+
+ // Ensure that transitions are done when a fragment is shown and hidden
+ @Test
+ fun showHideTransition() {
+ val fragment1 = setupInitialFragment()
+ val fragment2 = TransitionFragment(R.layout.scene2)
+
+ val startBlue = findBlue()
+ val startGreen = findGreen()
+
+ fragmentManager.beginTransaction()
+ .setReorderingAllowed(reorderingAllowed)
+ .add(R.id.fragmentContainer, fragment2)
+ .hide(fragment1)
+ .addToBackStack(null)
+ .commit()
+
+ FragmentTestUtil.waitForExecution(activityRule)
+ fragment1.waitForTransition()
+ fragment2.waitForTransition()
+
+ val endGreen = findViewById(fragment2, R.id.greenSquare)
+ val endBlue = findViewById(fragment2, R.id.blueSquare)
+
+ assertThat(fragment1.requireView().visibility).isEqualTo(View.GONE)
+ assertThat(startGreen.visibility).isEqualTo(View.VISIBLE)
+ assertThat(startBlue.visibility).isEqualTo(View.VISIBLE)
+
+ verifyAndClearTransition(fragment1.exitTransition, null, startGreen, startBlue)
+ verifyNoOtherTransitions(fragment1)
+
+ verifyAndClearTransition(fragment2.enterTransition, null, endGreen, endBlue)
+ verifyNoOtherTransitions(fragment2)
+
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+
+ FragmentTestUtil.waitForExecution(activityRule)
+ fragment1.waitForTransition()
+ fragment2.waitForTransition()
+
+ verifyAndClearTransition(fragment1.reenterTransition, null, startGreen, startBlue)
+ verifyNoOtherTransitions(fragment1)
+
+ assertThat(fragment1.requireView().visibility).isEqualTo(View.VISIBLE)
+ assertThat(startGreen.visibility).isEqualTo(View.VISIBLE)
+ assertThat(startBlue.visibility).isEqualTo(View.VISIBLE)
+
+ verifyAndClearTransition(fragment2.returnTransition, null, endGreen, endBlue)
+ verifyNoOtherTransitions(fragment2)
+ }
+
+ // Ensure that transitions are done when a fragment is attached and detached
+ @Test
+ fun attachDetachTransition() {
+ val fragment1 = setupInitialFragment()
+ val fragment2 = TransitionFragment(R.layout.scene2)
+
+ val startBlue = findBlue()
+ val startGreen = findGreen()
+
+ fragmentManager.beginTransaction()
+ .setReorderingAllowed(reorderingAllowed)
+ .add(R.id.fragmentContainer, fragment2)
+ .detach(fragment1)
+ .addToBackStack(null)
+ .commit()
+
+ FragmentTestUtil.waitForExecution(activityRule)
+
+ val endGreen = findViewById(fragment2, R.id.greenSquare)
+ val endBlue = findViewById(fragment2, R.id.blueSquare)
+
+ verifyAndClearTransition(fragment1.exitTransition, null, startGreen, startBlue)
+ verifyNoOtherTransitions(fragment1)
+
+ verifyAndClearTransition(fragment2.enterTransition, null, endGreen, endBlue)
+ verifyNoOtherTransitions(fragment2)
+
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+
+ FragmentTestUtil.waitForExecution(activityRule)
+
+ val reenterBlue = findBlue()
+ val reenterGreen = findGreen()
+
+ verifyAndClearTransition(fragment1.reenterTransition, null, reenterGreen, reenterBlue)
+ verifyNoOtherTransitions(fragment1)
+
+ verifyAndClearTransition(fragment2.returnTransition, null, endGreen, endBlue)
+ verifyNoOtherTransitions(fragment2)
+ }
+
+ // Ensure that shared element without matching transition name doesn't error out
+ @Test
+ fun sharedElementMismatch() {
+ val fragment1 = setupInitialFragment()
+
+ // Now do a transition to scene2
+ val fragment2 = TransitionFragment(R.layout.scene2)
+
+ val startBlue = findBlue()
+ val startGreen = findGreen()
+ val startBlueBounds = getBoundsOnScreen(startBlue)
+
+ fragmentManager.beginTransaction()
+ .addSharedElement(startBlue, "fooSquare")
+ .replace(R.id.fragmentContainer, fragment2)
+ .setReorderingAllowed(reorderingAllowed)
+ .addToBackStack(null)
+ .commit()
+ FragmentTestUtil.waitForExecution(activityRule)
+
+ fragment1.waitForTransition()
+ fragment2.waitForTransition()
+
+ val endBlue = findBlue()
+ val endGreen = findGreen()
+
+ if (reorderingAllowed) {
+ verifyAndClearTransition(fragment1.exitTransition, null, startGreen, startBlue)
+ } else {
+ verifyAndClearTransition(fragment1.exitTransition, startBlueBounds, startGreen)
+ verifyAndClearTransition(fragment2.sharedElementEnter, startBlueBounds, startBlue)
+ }
+ verifyNoOtherTransitions(fragment1)
+
+ verifyAndClearTransition(fragment2.enterTransition, null, endGreen, endBlue)
+ verifyNoOtherTransitions(fragment2)
+ }
+
+ // Ensure that using the same source or target shared element results in an exception.
+ @Test
+ fun sharedDuplicateTargetNames() {
+ setupInitialFragment()
+
+ val startBlue = findBlue()
+ val startGreen = findGreen()
+
+ val ft = fragmentManager.beginTransaction()
+ ft.addSharedElement(startBlue, "blueSquare")
+ try {
+ ft.addSharedElement(startGreen, "blueSquare")
+ fail("Expected IllegalArgumentException")
+ } catch (e: IllegalArgumentException) {
+ assertThat(e)
+ .hasMessageThat().contains("A shared element with the target name 'blueSquare' " +
+ "has already been added to the transaction.")
+ }
+
+ try {
+ ft.addSharedElement(startBlue, "greenSquare")
+ fail("Expected IllegalArgumentException")
+ } catch (e: IllegalArgumentException) {
+ assertThat(e)
+ .hasMessageThat().contains("A shared element with the source name 'blueSquare' " +
+ "has already been added to the transaction.")
+ }
+ }
+
+ // Test that invisible fragment views don't participate in transitions
+ @Test
+ fun invisibleNoTransitions() {
+ if (!reorderingAllowed) {
+ return // only reordered transitions can avoid interaction
+ }
+ // enter transition
+ val fragment = InvisibleFragment()
+ fragmentManager.beginTransaction()
+ .setReorderingAllowed(reorderingAllowed)
+ .add(R.id.fragmentContainer, fragment)
+ .addToBackStack(null)
+ .commit()
+ FragmentTestUtil.waitForExecution(activityRule)
+ fragment.waitForNoTransition()
+ verifyNoOtherTransitions(fragment)
+
+ // exit transition
+ fragmentManager.beginTransaction()
+ .setReorderingAllowed(reorderingAllowed)
+ .remove(fragment)
+ .addToBackStack(null)
+ .commit()
+
+ fragment.waitForNoTransition()
+ verifyNoOtherTransitions(fragment)
+
+ // reenter transition
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+ fragment.waitForNoTransition()
+ verifyNoOtherTransitions(fragment)
+
+ // return transition
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+ fragment.waitForNoTransition()
+ verifyNoOtherTransitions(fragment)
+ }
+
+ // No crash when transitioning a shared element and there is no shared element transition.
+ @Test
+ fun noSharedElementTransition() {
+ val fragment1 = setupInitialFragment()
+
+ val startBlue = findBlue()
+ val startGreen = findGreen()
+ val startBlueBounds = getBoundsOnScreen(startBlue)
+
+ val fragment2 = TransitionFragment(R.layout.scene2)
+
+ fragmentManager.beginTransaction()
+ .setReorderingAllowed(reorderingAllowed)
+ .addSharedElement(startBlue, "blueSquare")
+ .replace(R.id.fragmentContainer, fragment2)
+ .addToBackStack(null)
+ .commit()
+
+ fragment1.waitForTransition()
+ fragment2.waitForTransition()
+ val midGreen = findGreen()
+ val midBlue = findBlue()
+ val midBlueBounds = getBoundsOnScreen(midBlue)
+ verifyAndClearTransition(fragment1.exitTransition, startBlueBounds, startGreen)
+ verifyAndClearTransition(fragment2.sharedElementEnter, startBlueBounds, startBlue, midBlue)
+ verifyAndClearTransition(fragment2.enterTransition, midBlueBounds, midGreen)
+ verifyNoOtherTransitions(fragment1)
+ verifyNoOtherTransitions(fragment2)
+
+ val fragment3 = TransitionFragment(R.layout.scene3)
+
+ activityRule.runOnUiThread {
+ val fm = activityRule.activity.supportFragmentManager
+ fm.popBackStack()
+ fm.beginTransaction()
+ .setReorderingAllowed(reorderingAllowed)
+ .replace(R.id.fragmentContainer, fragment3)
+ .addToBackStack(null)
+ .commit()
+ }
+
+ // This shouldn't give an error.
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ fragment2.waitForTransition()
+ // It does not transition properly for ordered transactions, though.
+ if (reorderingAllowed) {
+ verifyAndClearTransition(fragment2.returnTransition, null, midGreen, midBlue)
+ val endGreen = findGreen()
+ val endBlue = findBlue()
+ val endRed = findRed()
+ verifyAndClearTransition(fragment3.enterTransition, null, endGreen, endBlue, endRed!!)
+ verifyNoOtherTransitions(fragment2)
+ verifyNoOtherTransitions(fragment3)
+ } else {
+ // fragment3 doesn't get a transition since it conflicts with the pop transition
+ verifyNoOtherTransitions(fragment3)
+ // Everything else is just doing its best. Ordered transactions can't handle
+ // multiple transitions acting together except for popping multiple together.
+ }
+ }
+
+ // When there is no matching shared element, the transition name should not be changed
+ @Test
+ fun noMatchingSharedElementRetainName() {
+ val fragment1 = setupInitialFragment()
+
+ val startBlue = findBlue()
+ val startGreen = findGreen()
+ val startGreenBounds = getBoundsOnScreen(startGreen)
+
+ val fragment2 = TransitionFragment(R.layout.scene3)
+
+ fragmentManager.beginTransaction()
+ .setReorderingAllowed(reorderingAllowed)
+ .addSharedElement(startGreen, "greenSquare")
+ .addSharedElement(startBlue, "blueSquare")
+ .replace(R.id.fragmentContainer, fragment2)
+ .addToBackStack(null)
+ .commit()
+
+ fragment2.waitForTransition()
+ val midGreen = findGreen()
+ val midBlue = findBlue()
+ val midRed = findRed()
+ val midGreenBounds = getBoundsOnScreen(midGreen)
+ if (reorderingAllowed) {
+ verifyAndClearTransition(
+ fragment2.sharedElementEnter, startGreenBounds, startGreen,
+ midGreen
+ )
+ } else {
+ verifyAndClearTransition(
+ fragment2.sharedElementEnter, startGreenBounds, startGreen,
+ midGreen, startBlue
+ )
+ }
+ verifyAndClearTransition(fragment2.enterTransition, midGreenBounds, midBlue, midRed!!)
+ verifyNoOtherTransitions(fragment2)
+
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+ fragment2.waitForTransition()
+ fragment1.waitForTransition()
+
+ val endBlue = findBlue()
+ val endGreen = findGreen()
+
+ assertThat(endBlue.transitionName).isEqualTo("blueSquare")
+ assertThat(endGreen.transitionName).isEqualTo("greenSquare")
+ }
+
+ private fun setupInitialFragment(): TransitionFragment {
+ val fragment1 = TransitionFragment(R.layout.scene1)
+ fragmentManager.beginTransaction()
+ .setReorderingAllowed(reorderingAllowed)
+ .add(R.id.fragmentContainer, fragment1)
+ .addToBackStack(null)
+ .commit()
+ FragmentTestUtil.waitForExecution(activityRule)
+ assertThat(onBackStackChangedTimes).isEqualTo(1)
+ fragment1.waitForTransition()
+ val blueSquare1 = findBlue()
+ val greenSquare1 = findGreen()
+ verifyAndClearTransition(fragment1.enterTransition, null, blueSquare1, greenSquare1)
+ verifyNoOtherTransitions(fragment1)
+ return fragment1
+ }
+
+ private fun findViewById(fragment: Fragment, id: Int): View {
+ return fragment.requireView().findViewById(id)
+ }
+
+ private fun findGreen(): View {
+ return activityRule.activity.findViewById(R.id.greenSquare)
+ }
+
+ private fun findBlue(): View {
+ return activityRule.activity.findViewById(R.id.blueSquare)
+ }
+
+ private fun findRed(): View? {
+ return activityRule.activity.findViewById(R.id.redSquare)
+ }
+
+ private fun verifyAndClearTransition(
+ transition: TargetTracking,
+ epicenter: Rect?,
+ vararg expected: View
+ ) {
+ if (epicenter == null) {
+ assertThat(transition.capturedEpicenter).isNull()
+ } else {
+ assertThat(transition.capturedEpicenter).isEqualTo(epicenter)
+ }
+ val targets = transition.trackedTargets
+ val sb = StringBuilder()
+ sb.append("Expected: [")
+ .append(expected.size)
+ .append("] {")
+ var isFirst = true
+ for (view in expected) {
+ if (isFirst) {
+ isFirst = false
+ } else {
+ sb.append(", ")
+ }
+ sb.append(view)
+ }
+ sb.append("}, but got: [")
+ .append(targets.size)
+ .append("] {")
+ isFirst = true
+ for (view in targets) {
+ if (isFirst) {
+ isFirst = false
+ } else {
+ sb.append(", ")
+ }
+ sb.append(view)
+ }
+ sb.append("}")
+ val errorMessage = sb.toString()
+
+ assertWithMessage(errorMessage).that(targets.size).isEqualTo(expected.size)
+ for (view in expected) {
+ assertWithMessage(errorMessage).that(targets.contains(view)).isTrue()
+ }
+ transition.clearTargets()
+ }
+
+ private fun verifyNoOtherTransitions(fragment: TransitionFragment) {
+ assertThat(fragment.enterTransition.targets.size).isEqualTo(0)
+ assertThat(fragment.exitTransition.targets.size).isEqualTo(0)
+ assertThat(fragment.reenterTransition.targets.size).isEqualTo(0)
+ assertThat(fragment.returnTransition.targets.size).isEqualTo(0)
+ assertThat(fragment.sharedElementEnter.targets.size).isEqualTo(0)
+ assertThat(fragment.sharedElementReturn.targets.size).isEqualTo(0)
+ }
+
+ private fun verifyTransition(
+ from: TransitionFragment,
+ to: TransitionFragment,
+ sharedElementName: String
+ ) {
+ val startOnBackStackChanged = onBackStackChangedTimes
+ val startBlue = findBlue()
+ val startGreen = findGreen()
+ val startRed = findRed()
+
+ val startBlueRect = getBoundsOnScreen(startBlue)
+
+ fragmentManager.beginTransaction()
+ .setReorderingAllowed(reorderingAllowed)
+ .addSharedElement(startBlue, sharedElementName)
+ .replace(R.id.fragmentContainer, to)
+ .addToBackStack(null)
+ .commit()
+
+ FragmentTestUtil.waitForExecution(activityRule)
+ assertThat(onBackStackChangedTimes).isEqualTo(startOnBackStackChanged + 1)
+
+ to.waitForTransition()
+ val endGreen = findGreen()
+ val endBlue = findBlue()
+ val endRed = findRed()
+ val endBlueRect = getBoundsOnScreen(endBlue)
+
+ if (startRed != null) {
+ verifyAndClearTransition(from.exitTransition, startBlueRect, startGreen, startRed)
+ } else {
+ verifyAndClearTransition(from.exitTransition, startBlueRect, startGreen)
+ }
+ verifyNoOtherTransitions(from)
+
+ if (endRed != null) {
+ verifyAndClearTransition(to.enterTransition, endBlueRect, endGreen, endRed)
+ } else {
+ verifyAndClearTransition(to.enterTransition, endBlueRect, endGreen)
+ }
+ verifyAndClearTransition(to.sharedElementEnter, startBlueRect, startBlue, endBlue)
+ verifyNoOtherTransitions(to)
+ }
+
+ private fun verifyCrossTransition(
+ swapSource: Boolean,
+ from1: TransitionFragment,
+ from2: TransitionFragment
+ ) {
+ val startNumOnBackStackChanged = onBackStackChangedTimes
+ val changesPerOperation = if (reorderingAllowed) 1 else 2
+
+ val to1 = TransitionFragment(R.layout.scene2)
+ val to2 = TransitionFragment(R.layout.scene2)
+
+ val fromExit1 = findViewById(from1, R.id.greenSquare)
+ val fromShared1 = findViewById(from1, R.id.blueSquare)
+ val fromSharedRect1 = getBoundsOnScreen(fromShared1)
+
+ val fromExitId2 = if (swapSource) R.id.blueSquare else R.id.greenSquare
+ val fromSharedId2 = if (swapSource) R.id.greenSquare else R.id.blueSquare
+ val fromExit2 = findViewById(from2, fromExitId2)
+ val fromShared2 = findViewById(from2, fromSharedId2)
+ val fromSharedRect2 = getBoundsOnScreen(fromShared2)
+
+ val sharedElementName = if (swapSource) "blueSquare" else "greenSquare"
+
+ activityRule.runOnUiThread {
+ fragmentManager.beginTransaction()
+ .setReorderingAllowed(reorderingAllowed)
+ .addSharedElement(fromShared1, "blueSquare")
+ .replace(R.id.fragmentContainer1, to1)
+ .addToBackStack(null)
+ .commit()
+ fragmentManager.beginTransaction()
+ .setReorderingAllowed(reorderingAllowed)
+ .addSharedElement(fromShared2, sharedElementName)
+ .replace(R.id.fragmentContainer2, to2)
+ .addToBackStack(null)
+ .commit()
+ }
+ FragmentTestUtil.waitForExecution(activityRule)
+ assertThat(onBackStackChangedTimes)
+ .isEqualTo(startNumOnBackStackChanged + changesPerOperation)
+
+ from1.waitForTransition()
+ from2.waitForTransition()
+ to1.waitForTransition()
+ to2.waitForTransition()
+
+ val toEnter1 = findViewById(to1, R.id.greenSquare)
+ val toShared1 = findViewById(to1, R.id.blueSquare)
+ val toSharedRect1 = getBoundsOnScreen(toShared1)
+
+ val toEnter2 = findViewById(to2, fromSharedId2)
+ val toShared2 = findViewById(to2, fromExitId2)
+ val toSharedRect2 = getBoundsOnScreen(toShared2)
+
+ verifyAndClearTransition(from1.exitTransition, fromSharedRect1, fromExit1)
+ verifyAndClearTransition(from2.exitTransition, fromSharedRect2, fromExit2)
+ verifyNoOtherTransitions(from1)
+ verifyNoOtherTransitions(from2)
+
+ verifyAndClearTransition(to1.enterTransition, toSharedRect1, toEnter1)
+ verifyAndClearTransition(to2.enterTransition, toSharedRect2, toEnter2)
+ verifyAndClearTransition(to1.sharedElementEnter, fromSharedRect1, fromShared1, toShared1)
+ verifyAndClearTransition(to2.sharedElementEnter, fromSharedRect2, fromShared2, toShared2)
+ verifyNoOtherTransitions(to1)
+ verifyNoOtherTransitions(to2)
+
+ // Now pop it back
+ activityRule.runOnUiThread {
+ fragmentManager.popBackStack()
+ fragmentManager.popBackStack()
+ }
+ FragmentTestUtil.waitForExecution(activityRule)
+ assertThat(onBackStackChangedTimes)
+ .isEqualTo(startNumOnBackStackChanged + changesPerOperation + 1)
+
+ from1.waitForTransition()
+ from2.waitForTransition()
+ to1.waitForTransition()
+ to2.waitForTransition()
+
+ val returnEnter1 = findViewById(from1, R.id.greenSquare)
+ val returnShared1 = findViewById(from1, R.id.blueSquare)
+
+ val returnEnter2 = findViewById(from2, fromExitId2)
+ val returnShared2 = findViewById(from2, fromSharedId2)
+
+ verifyAndClearTransition(to1.returnTransition, toSharedRect1, toEnter1)
+ verifyAndClearTransition(to2.returnTransition, toSharedRect2, toEnter2)
+ verifyAndClearTransition(to1.sharedElementReturn, toSharedRect1, toShared1, returnShared1)
+ verifyAndClearTransition(to2.sharedElementReturn, toSharedRect2, toShared2, returnShared2)
+ verifyNoOtherTransitions(to1)
+ verifyNoOtherTransitions(to2)
+
+ verifyAndClearTransition(from1.reenterTransition, fromSharedRect1, returnEnter1)
+ verifyAndClearTransition(from2.reenterTransition, fromSharedRect2, returnEnter2)
+ verifyNoOtherTransitions(from1)
+ verifyNoOtherTransitions(from2)
+ }
+
+ private fun verifyPopTransition(
+ numPops: Int,
+ from: TransitionFragment,
+ to: TransitionFragment,
+ vararg others: TransitionFragment
+ ) {
+ val startOnBackStackChanged = onBackStackChangedTimes
+ val startBlue = findBlue()
+ val startGreen = findGreen()
+ val startRed = findRed()
+ val startSharedRect = getBoundsOnScreen(startBlue)
+
+ instrumentation.runOnMainSync {
+ for (i in 0 until numPops) {
+ fragmentManager.popBackStack()
+ }
+ }
+ FragmentTestUtil.waitForExecution(activityRule)
+ assertThat(onBackStackChangedTimes).isEqualTo((startOnBackStackChanged + 1))
+
+ to.waitForTransition()
+ val endGreen = findGreen()
+ val endBlue = findBlue()
+ val endRed = findRed()
+ val endSharedRect = getBoundsOnScreen(endBlue)
+
+ if (startRed != null) {
+ verifyAndClearTransition(from.returnTransition, startSharedRect, startGreen, startRed)
+ } else {
+ verifyAndClearTransition(from.returnTransition, startSharedRect, startGreen)
+ }
+ verifyAndClearTransition(from.sharedElementReturn, startSharedRect, startBlue, endBlue)
+ verifyNoOtherTransitions(from)
+
+ if (endRed != null) {
+ verifyAndClearTransition(to.reenterTransition, endSharedRect, endGreen, endRed)
+ } else {
+ verifyAndClearTransition(to.reenterTransition, endSharedRect, endGreen)
+ }
+ verifyNoOtherTransitions(to)
+
+ for (fragment in others) {
+ verifyNoOtherTransitions(fragment)
+ }
+ }
+
+ class ComplexTransitionFragment : TransitionFragment(R.layout.scene2) {
+ val sharedElementEnterTransition1 = TrackingTransition()
+ val sharedElementEnterTransition2 = TrackingTransition()
+ val sharedElementReturnTransition1 = TrackingTransition()
+ val sharedElementReturnTransition2 = TrackingTransition()
+
+ val sharedElementEnterTransition: TransitionSet = TransitionSet()
+ .addTransition(sharedElementEnterTransition1)
+ .addTransition(sharedElementEnterTransition2)
+ val sharedElementReturnTransition: TransitionSet = TransitionSet()
+ .addTransition(sharedElementReturnTransition1)
+ .addTransition(sharedElementReturnTransition2)
+
+ init {
+ sharedElementEnterTransition1.addTarget(R.id.blueSquare)
+ sharedElementEnterTransition2.addTarget(R.id.greenSquare)
+ sharedElementReturnTransition1.addTarget(R.id.blueSquare)
+ sharedElementReturnTransition2.addTarget(R.id.greenSquare)
+ setSharedElementEnterTransition(sharedElementEnterTransition)
+ setSharedElementReturnTransition(sharedElementReturnTransition)
+ }
+ }
+
+ class InvisibleFragment : TransitionFragment(R.layout.scene1) {
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ view.visibility = View.INVISIBLE
+ super.onViewCreated(view, savedInstanceState)
+ }
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters
+ fun data(): Array<Boolean> {
+ return arrayOf(false, true)
+ }
+
+ private fun getBoundsOnScreen(view: View): Rect {
+ val loc = IntArray(2)
+ view.getLocationOnScreen(loc)
+ return Rect(loc[0], loc[1], loc[0] + view.width, loc[1] + view.height)
+ }
+ }
+}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleTest.kt
index a8379f2..be3612c 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleTest.kt
+++ b/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleTest.kt
@@ -56,8 +56,7 @@
val activity = activityRule.activity
val fm = activity.supportFragmentManager
- val fragment = StrictViewFragment()
- fragment.setLayoutId(R.layout.fragment_a)
+ val fragment = StrictViewFragment(R.layout.fragment_a)
fm.beginTransaction().add(R.id.content, fragment).commitNow()
assertThat(fragment.viewLifecycleOwner.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
@@ -108,8 +107,7 @@
val fm = activity.supportFragmentManager
val countDownLatch = CountDownLatch(1)
- val fragment = StrictViewFragment()
- fragment.setLayoutId(R.layout.fragment_a)
+ val fragment = StrictViewFragment(R.layout.fragment_a)
fm.beginTransaction().add(R.id.content, fragment).runOnCommit {
assertThat(fragment.viewLifecycleOwner.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
@@ -124,21 +122,20 @@
val fm = activity.supportFragmentManager
val countDownLatch = CountDownLatch(2)
- val fragment = StrictViewFragment()
- fragment.setLayoutId(R.layout.fragment_a)
+ val fragment = StrictViewFragment(R.layout.fragment_a)
activityRule.runOnUiThread {
fragment.viewLifecycleOwnerLiveData.observe(activity,
Observer { lifecycleOwner ->
if (lifecycleOwner != null) {
assertWithMessage("Fragment View LifecycleOwner should be only be set" +
"after onCreateView()")
- .that(fragment.mOnCreateViewCalled)
+ .that(fragment.onCreateViewCalled)
.isTrue()
countDownLatch.countDown()
} else {
assertWithMessage("Fragment View LifecycleOwner should be set to null" +
" after onDestroyView()")
- .that(fragment.mOnDestroyViewCalled)
+ .that(fragment.onDestroyViewCalled)
.isTrue()
countDownLatch.countDown()
}
@@ -155,8 +152,7 @@
val activity = activityRule.activity
val fm = activity.supportFragmentManager
- val fragment = StrictViewFragment()
- fragment.setLayoutId(R.layout.fragment_a)
+ val fragment = StrictViewFragment(R.layout.fragment_a)
val lifecycleObserver = mock(LifecycleEventObserver::class.java)
lateinit var viewLifecycleOwner: LifecycleOwner
activityRule.runOnUiThread {
@@ -197,7 +193,6 @@
val fm = activity.supportFragmentManager
val fragment = ObservingFragment()
- fragment.setLayoutId(R.layout.fragment_a)
fm.beginTransaction().add(R.id.content, fragment).commitNow()
val viewLifecycleOwner = fragment.viewLifecycleOwner
assertThat(viewLifecycleOwner.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
@@ -225,7 +220,6 @@
val fm = activity.supportFragmentManager
val fragment = ObservingFragment()
- fragment.setLayoutId(R.layout.fragment_a)
fm.beginTransaction().add(R.id.content, fragment).commitNow()
val viewLifecycleOwner = fragment.viewLifecycleOwner
assertThat(viewLifecycleOwner.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
@@ -264,7 +258,7 @@
}
}
- class ObservingFragment : StrictViewFragment() {
+ class ObservingFragment : StrictViewFragment(R.layout.fragment_a) {
val liveData = MutableLiveData<Boolean>()
private val onCreateViewObserver = Observer<Boolean> { }
private val onViewCreatedObserver = Observer<Boolean> { }
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewTest.kt
new file mode 100644
index 0000000..c1d4d40
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewTest.kt
@@ -0,0 +1,1045 @@
+/*
+ * 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 androidx.fragment.app
+
+import org.junit.Assert.fail
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+
+import androidx.annotation.ContentView
+import androidx.fragment.app.test.FragmentTestActivity
+import androidx.fragment.test.R
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.ActivityTestRule
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FragmentViewTest {
+ @get:Rule
+ val activityRule = ActivityTestRule(FragmentTestActivity::class.java)
+
+ private val instrumentation = InstrumentationRegistry.getInstrumentation()
+
+ // Test that adding a fragment adds the Views in the proper order. Popping the back stack
+ // should remove the correct Views.
+ @Test
+ fun addFragments() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val container =
+ activityRule.activity.findViewById<View>(R.id.fragmentContainer) as ViewGroup
+ val fm = activityRule.activity.supportFragmentManager
+
+ // One fragment with a view
+ val fragment1 = StrictViewFragment()
+ fm.beginTransaction().add(R.id.fragmentContainer, fragment1).addToBackStack(null).commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ FragmentTestUtil.assertChildren(container, fragment1)
+
+ // Add another on top
+ val fragment2 = StrictViewFragment()
+ fm.beginTransaction().add(R.id.fragmentContainer, fragment2).addToBackStack(null).commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ FragmentTestUtil.assertChildren(container, fragment1, fragment2)
+
+ // Now add two in one transaction:
+ val fragment3 = StrictViewFragment()
+ val fragment4 = StrictViewFragment()
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment3)
+ .add(R.id.fragmentContainer, fragment4)
+ .addToBackStack(null)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ FragmentTestUtil.assertChildren(container, fragment1, fragment2, fragment3, fragment4)
+
+ fm.popBackStack()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ FragmentTestUtil.assertChildren(container, fragment1, fragment2)
+
+ fm.popBackStack()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ assertThat(container.childCount).isEqualTo(1)
+ FragmentTestUtil.assertChildren(container, fragment1)
+
+ fm.popBackStack()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ FragmentTestUtil.assertChildren(container)
+ }
+
+ // Add fragments to multiple containers in the same transaction. Make sure that
+ // they pop correctly, too.
+ @Test
+ fun addTwoContainers() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.double_container)
+ val container1 =
+ activityRule.activity.findViewById<View>(R.id.fragmentContainer1) as ViewGroup
+ val container2 =
+ activityRule.activity.findViewById<View>(R.id.fragmentContainer2) as ViewGroup
+ val fm = activityRule.activity.supportFragmentManager
+
+ val fragment1 = StrictViewFragment()
+ fm.beginTransaction().add(R.id.fragmentContainer1, fragment1).addToBackStack(null).commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ FragmentTestUtil.assertChildren(container1, fragment1)
+
+ val fragment2 = StrictViewFragment()
+ fm.beginTransaction().add(R.id.fragmentContainer2, fragment2).addToBackStack(null).commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ FragmentTestUtil.assertChildren(container2, fragment2)
+
+ val fragment3 = StrictViewFragment()
+ val fragment4 = StrictViewFragment()
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer1, fragment3)
+ .add(R.id.fragmentContainer2, fragment4)
+ .addToBackStack(null)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container1, fragment1, fragment3)
+ FragmentTestUtil.assertChildren(container2, fragment2, fragment4)
+
+ fm.popBackStack()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ FragmentTestUtil.assertChildren(container1, fragment1)
+ FragmentTestUtil.assertChildren(container2, fragment2)
+
+ fm.popBackStack()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ FragmentTestUtil.assertChildren(container1, fragment1)
+ FragmentTestUtil.assertChildren(container2)
+
+ fm.popBackStack()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ assertThat(container1.childCount).isEqualTo(0)
+ }
+
+ // When you add a fragment that's has already been added, it should throw.
+ @Test
+ fun doubleAdd() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val fm = activityRule.activity.supportFragmentManager
+ val fragment1 = StrictViewFragment()
+ fm.beginTransaction().add(R.id.fragmentContainer, fragment1).commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ instrumentation.runOnMainSync {
+ try {
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment1)
+ .addToBackStack(null)
+ .commit()
+ fm.executePendingTransactions()
+ fail("Adding a fragment that is already added should be an error")
+ } catch (e: IllegalStateException) {
+ assertThat(e)
+ .hasMessageThat().contains("Fragment already added: $fragment1")
+ }
+ }
+ }
+
+ // Make sure that removed fragments remove the right Views. Popping the back stack should
+ // add the Views back properly
+ @Test
+ fun removeFragments() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val container =
+ activityRule.activity.findViewById<View>(R.id.fragmentContainer) as ViewGroup
+ val fm = activityRule.activity.supportFragmentManager
+ val fragment1 = StrictViewFragment()
+ val fragment2 = StrictViewFragment()
+ val fragment3 = StrictViewFragment()
+ val fragment4 = StrictViewFragment()
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment1, "1")
+ .add(R.id.fragmentContainer, fragment2, "2")
+ .add(R.id.fragmentContainer, fragment3, "3")
+ .add(R.id.fragmentContainer, fragment4, "4")
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ FragmentTestUtil.assertChildren(container, fragment1, fragment2, fragment3, fragment4)
+
+ // Remove a view
+ fm.beginTransaction().remove(fragment4).addToBackStack(null).commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ assertThat(container.childCount).isEqualTo(3)
+ FragmentTestUtil.assertChildren(container, fragment1, fragment2, fragment3)
+
+ // remove another one
+ fm.beginTransaction().remove(fragment2).addToBackStack(null).commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ FragmentTestUtil.assertChildren(container, fragment1, fragment3)
+
+ // Now remove the remaining:
+ fm.beginTransaction()
+ .remove(fragment3)
+ .remove(fragment1)
+ .addToBackStack(null)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ FragmentTestUtil.assertChildren(container)
+
+ fm.popBackStack()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ val replacement1 = fm.findFragmentByTag("1")
+ val replacement3 = fm.findFragmentByTag("3")
+ FragmentTestUtil.assertChildren(container, replacement1, replacement3)
+
+ fm.popBackStack()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ val replacement2 = fm.findFragmentByTag("2")
+ FragmentTestUtil.assertChildren(container, replacement1, replacement3, replacement2)
+
+ fm.popBackStack()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ val replacement4 = fm.findFragmentByTag("4")
+ FragmentTestUtil.assertChildren(
+ container, replacement1, replacement3, replacement2,
+ replacement4
+ )
+ }
+
+ // Removing a hidden fragment should remove the View and popping should bring it back hidden
+ @Test
+ fun removeHiddenView() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val container =
+ activityRule.activity.findViewById<View>(R.id.fragmentContainer) as ViewGroup
+ val fm = activityRule.activity.supportFragmentManager
+ val fragment1 = StrictViewFragment()
+ fm.beginTransaction().add(R.id.fragmentContainer, fragment1, "1").hide(fragment1).commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ FragmentTestUtil.assertChildren(container, fragment1)
+ assertThat(fragment1.isHidden).isTrue()
+
+ fm.beginTransaction().remove(fragment1).addToBackStack(null).commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ FragmentTestUtil.assertChildren(container)
+
+ fm.popBackStack()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ val replacement1 = fm.findFragmentByTag("1")!!
+ FragmentTestUtil.assertChildren(container, replacement1)
+ assertThat(replacement1.isHidden).isTrue()
+ assertThat(replacement1.requireView().visibility).isEqualTo(View.GONE)
+ }
+
+ // Removing a detached fragment should do nothing to the View and popping should bring
+ // the Fragment back detached
+ @Test
+ fun removeDetatchedView() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val container =
+ activityRule.activity.findViewById<View>(R.id.fragmentContainer) as ViewGroup
+ val fm = activityRule.activity.supportFragmentManager
+ val fragment1 = StrictViewFragment()
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment1, "1")
+ .detach(fragment1)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ FragmentTestUtil.assertChildren(container)
+ assertThat(fragment1.isDetached).isTrue()
+
+ fm.beginTransaction().remove(fragment1).addToBackStack(null).commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ FragmentTestUtil.assertChildren(container)
+
+ fm.popBackStack()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ val replacement1 = fm.findFragmentByTag("1")!!
+ FragmentTestUtil.assertChildren(container)
+ assertThat(replacement1.isDetached).isTrue()
+ }
+
+ // Unlike adding the same fragment twice, you should be able to add and then remove and then
+ // add the same fragment in one transaction.
+ @Test
+ fun addRemoveAdd() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val container =
+ activityRule.activity.findViewById<View>(R.id.fragmentContainer) as ViewGroup
+ val fm = activityRule.activity.supportFragmentManager
+ val fragment = StrictViewFragment()
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment)
+ .remove(fragment)
+ .add(R.id.fragmentContainer, fragment)
+ .addToBackStack(null)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ FragmentTestUtil.assertChildren(container, fragment)
+
+ fm.popBackStack()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ FragmentTestUtil.assertChildren(container)
+ }
+
+ // Removing a fragment that isn't in should not throw
+ @Test
+ fun removeNotThere() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val fm = activityRule.activity.supportFragmentManager
+ val fragment = StrictViewFragment()
+ fm.beginTransaction().remove(fragment).commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ }
+
+ // Hide a fragment and its View should be GONE. Then pop it and the View should be VISIBLE
+ @Test
+ fun hideFragment() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val container =
+ activityRule.activity.findViewById<View>(R.id.fragmentContainer) as ViewGroup
+ val fm = activityRule.activity.supportFragmentManager
+ val fragment = StrictViewFragment()
+ fm.beginTransaction().add(R.id.fragmentContainer, fragment).commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container, fragment)
+ assertThat(fragment.requireView().visibility).isEqualTo(View.VISIBLE)
+
+ fm.beginTransaction().hide(fragment).addToBackStack(null).commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container, fragment)
+ assertThat(fragment.isHidden).isTrue()
+ assertThat(fragment.requireView().visibility).isEqualTo(View.GONE)
+
+ fm.popBackStack()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container, fragment)
+ assertThat(fragment.isHidden).isFalse()
+ assertThat(fragment.requireView().visibility).isEqualTo(View.VISIBLE)
+ }
+
+ // Hiding a hidden fragment should not throw
+ @Test
+ fun doubleHide() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val fm = activityRule.activity.supportFragmentManager
+ val fragment = StrictViewFragment()
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment)
+ .hide(fragment)
+ .hide(fragment)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ }
+
+ // Hiding a non-existing fragment should not throw
+ @Test
+ fun hideUnAdded() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val fm = activityRule.activity.supportFragmentManager
+ val fragment = StrictViewFragment()
+ fm.beginTransaction()
+ .hide(fragment)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ }
+
+ // Show a hidden fragment and its View should be VISIBLE. Then pop it and the View should be
+ // GONE.
+ @Test
+ fun showFragment() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val container =
+ activityRule.activity.findViewById<View>(R.id.fragmentContainer) as ViewGroup
+ val fm = activityRule.activity.supportFragmentManager
+ val fragment = StrictViewFragment()
+ fm.beginTransaction().add(R.id.fragmentContainer, fragment).hide(fragment).commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container, fragment)
+ assertThat(fragment.isHidden).isTrue()
+ assertThat(fragment.requireView().visibility).isEqualTo(View.GONE)
+
+ fm.beginTransaction().show(fragment).addToBackStack(null).commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container, fragment)
+ assertThat(fragment.isHidden).isFalse()
+ assertThat(fragment.requireView().visibility).isEqualTo(View.VISIBLE)
+
+ fm.popBackStack()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container, fragment)
+ assertThat(fragment.isHidden).isTrue()
+ assertThat(fragment.requireView().visibility).isEqualTo(View.GONE)
+ }
+
+ // Showing a shown fragment should not throw
+ @Test
+ fun showShown() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val fm = activityRule.activity.supportFragmentManager
+ val fragment = StrictViewFragment()
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment)
+ .show(fragment)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ }
+
+ // Showing a non-existing fragment should not throw
+ @Test
+ fun showUnAdded() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val fm = activityRule.activity.supportFragmentManager
+ val fragment = StrictViewFragment()
+ fm.beginTransaction()
+ .show(fragment)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ }
+
+ // Detaching a fragment should remove the View from the hierarchy. Then popping it should
+ // bring it back VISIBLE
+ @Test
+ fun detachFragment() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val container =
+ activityRule.activity.findViewById<View>(R.id.fragmentContainer) as ViewGroup
+ val fm = activityRule.activity.supportFragmentManager
+ val fragment = StrictViewFragment()
+ fm.beginTransaction().add(R.id.fragmentContainer, fragment).commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container, fragment)
+ assertThat(fragment.isDetached).isFalse()
+ assertThat(fragment.requireView().visibility).isEqualTo(View.VISIBLE)
+
+ fm.beginTransaction().detach(fragment).addToBackStack(null).commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container)
+ assertThat(fragment.isDetached).isTrue()
+
+ fm.popBackStack()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container, fragment)
+ assertThat(fragment.isDetached).isFalse()
+ assertThat(fragment.requireView().visibility).isEqualTo(View.VISIBLE)
+ }
+
+ // Detaching a hidden fragment should remove the View from the hierarchy. Then popping it should
+ // bring it back hidden
+ @Test
+ fun detachHiddenFragment() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val container =
+ activityRule.activity.findViewById<View>(R.id.fragmentContainer) as ViewGroup
+ val fm = activityRule.activity.supportFragmentManager
+ val fragment = StrictViewFragment()
+ fm.beginTransaction().add(R.id.fragmentContainer, fragment).hide(fragment).commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container, fragment)
+ assertThat(fragment.isDetached).isFalse()
+ assertThat(fragment.isHidden).isTrue()
+ assertThat(fragment.requireView().visibility).isEqualTo(View.GONE)
+
+ fm.beginTransaction().detach(fragment).addToBackStack(null).commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container)
+ assertThat(fragment.isHidden).isTrue()
+ assertThat(fragment.isDetached).isTrue()
+
+ fm.popBackStack()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container, fragment)
+ assertThat(fragment.isHidden).isTrue()
+ assertThat(fragment.isDetached).isFalse()
+ assertThat(fragment.requireView().visibility).isEqualTo(View.GONE)
+ }
+
+ // Detaching a detached fragment should not throw
+ @Test
+ fun detachDetatched() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val fm = activityRule.activity.supportFragmentManager
+ val fragment = StrictViewFragment()
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment)
+ .detach(fragment)
+ .detach(fragment)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ }
+
+ // Detaching a non-existing fragment should not throw
+ @Test
+ fun detachUnAdded() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val fm = activityRule.activity.supportFragmentManager
+ val fragment = StrictViewFragment()
+ fm.beginTransaction()
+ .detach(fragment)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ }
+
+ // Attaching a fragment should add the View back into the hierarchy. Then popping it should
+ // remove it again
+ @Test
+ fun attachFragment() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val container =
+ activityRule.activity.findViewById<View>(R.id.fragmentContainer) as ViewGroup
+ val fm = activityRule.activity.supportFragmentManager
+ val fragment = StrictViewFragment()
+ fm.beginTransaction().add(R.id.fragmentContainer, fragment).detach(fragment).commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container)
+ assertThat(fragment.isDetached).isTrue()
+
+ fm.beginTransaction().attach(fragment).addToBackStack(null).commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container, fragment)
+ assertThat(fragment.isDetached).isFalse()
+ assertThat(fragment.requireView().visibility).isEqualTo(View.VISIBLE)
+
+ fm.popBackStack()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container)
+ assertThat(fragment.isDetached).isTrue()
+ }
+
+ // Attaching a hidden fragment should add the View as GONE the hierarchy. Then popping it should
+ // remove it again.
+ @Test
+ fun attachHiddenFragment() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val container =
+ activityRule.activity.findViewById<View>(R.id.fragmentContainer) as ViewGroup
+ val fm = activityRule.activity.supportFragmentManager
+ val fragment = StrictViewFragment()
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment)
+ .hide(fragment)
+ .detach(fragment)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container)
+ assertThat(fragment.isDetached).isTrue()
+ assertThat(fragment.isHidden).isTrue()
+
+ fm.beginTransaction().attach(fragment).addToBackStack(null).commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container, fragment)
+ assertThat(fragment.isHidden).isTrue()
+ assertThat(fragment.isDetached).isFalse()
+ assertThat(fragment.requireView().visibility).isEqualTo(View.GONE)
+
+ fm.popBackStack()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container)
+ assertThat(fragment.isDetached).isTrue()
+ assertThat(fragment.isHidden).isTrue()
+ }
+
+ // Attaching an attached fragment should not throw
+ @Test
+ fun attachAttached() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val fm = activityRule.activity.supportFragmentManager
+ val fragment = StrictViewFragment()
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment)
+ .attach(fragment)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ }
+
+ // Attaching a non-existing fragment should not throw
+ @Test
+ fun attachUnAdded() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val fm = activityRule.activity.supportFragmentManager
+ val fragment = StrictViewFragment()
+ fm.beginTransaction()
+ .attach(fragment)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ }
+
+ // Simple replace of one fragment in a container. Popping should replace it back again
+ @Test
+ fun replaceOne() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val container =
+ activityRule.activity.findViewById<View>(R.id.fragmentContainer) as ViewGroup
+ val fm = activityRule.activity.supportFragmentManager
+ val fragment1 = StrictViewFragment()
+ fm.beginTransaction().add(R.id.fragmentContainer, fragment1, "1").commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container, fragment1)
+
+ val fragment2 = StrictViewFragment()
+ fm.beginTransaction()
+ .replace(R.id.fragmentContainer, fragment2)
+ .addToBackStack(null)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container, fragment2)
+ assertThat(fragment2.requireView().visibility).isEqualTo(View.VISIBLE)
+
+ fm.popBackStack()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ val replacement1 = fm.findFragmentByTag("1")!!
+ assertThat(replacement1).isNotNull()
+ FragmentTestUtil.assertChildren(container, replacement1)
+ assertThat(replacement1.isHidden).isFalse()
+ assertThat(replacement1.isAdded).isTrue()
+ assertThat(replacement1.isDetached).isFalse()
+ assertThat(replacement1.requireView().visibility).isEqualTo(View.VISIBLE)
+ }
+
+ // Replace of multiple fragments in a container. Popping should replace it back again
+ @Test
+ fun replaceTwo() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val container =
+ activityRule.activity.findViewById<View>(R.id.fragmentContainer) as ViewGroup
+ val fm = activityRule.activity.supportFragmentManager
+ val fragment1 = StrictViewFragment()
+ val fragment2 = StrictViewFragment()
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment1, "1")
+ .add(R.id.fragmentContainer, fragment2, "2")
+ .hide(fragment2)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container, fragment1, fragment2)
+
+ val fragment3 = StrictViewFragment()
+ fm.beginTransaction()
+ .replace(R.id.fragmentContainer, fragment3)
+ .addToBackStack(null)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container, fragment3)
+ assertThat(fragment3.requireView().visibility).isEqualTo(View.VISIBLE)
+
+ fm.popBackStack()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ val replacement1 = fm.findFragmentByTag("1")!!
+ val replacement2 = fm.findFragmentByTag("2")!!
+ assertThat(replacement1).isNotNull()
+ assertThat(replacement2).isNotNull()
+ FragmentTestUtil.assertChildren(container, replacement1, replacement2)
+ assertThat(replacement1.isHidden).isFalse()
+ assertThat(replacement1.isAdded).isTrue()
+ assertThat(replacement1.isDetached).isFalse()
+ assertThat(replacement1.requireView().visibility).isEqualTo(View.VISIBLE)
+
+ // fragment2 was hidden, so it should be returned hidden
+ assertThat(replacement2.isHidden).isTrue()
+ assertThat(replacement2.isAdded).isTrue()
+ assertThat(replacement2.isDetached).isFalse()
+ assertThat(replacement2.requireView().visibility).isEqualTo(View.GONE)
+ }
+
+ // Replace of empty container. Should act as add and popping should just remove the fragment
+ @Test
+ fun replaceZero() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val container =
+ activityRule.activity.findViewById<View>(R.id.fragmentContainer) as ViewGroup
+ val fm = activityRule.activity.supportFragmentManager
+
+ val fragment = StrictViewFragment()
+ fm.beginTransaction()
+ .replace(R.id.fragmentContainer, fragment)
+ .addToBackStack(null)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container, fragment)
+ assertThat(fragment.requireView().visibility).isEqualTo(View.VISIBLE)
+
+ fm.popBackStack()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container)
+ }
+
+ // Replace a fragment that exists with itself
+ @Test
+ fun replaceExisting() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val container =
+ activityRule.activity.findViewById<View>(R.id.fragmentContainer) as ViewGroup
+ val fm = activityRule.activity.supportFragmentManager
+ val fragment1 = StrictViewFragment()
+ val fragment2 = StrictViewFragment()
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment1, "1")
+ .add(R.id.fragmentContainer, fragment2, "2")
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container, fragment1, fragment2)
+
+ fm.beginTransaction()
+ .replace(R.id.fragmentContainer, fragment1)
+ .addToBackStack(null)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container, fragment1)
+
+ fm.popBackStack()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ val replacement1 = fm.findFragmentByTag("1")
+ val replacement2 = fm.findFragmentByTag("2")
+
+ assertThat(replacement1).isSameAs(fragment1)
+ FragmentTestUtil.assertChildren(container, replacement1, replacement2)
+ }
+
+ // Have two replace operations in the same transaction to ensure that they
+ // don't interfere with each other
+ @Test
+ fun replaceReplace() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.double_container)
+ val container1 =
+ activityRule.activity.findViewById<View>(R.id.fragmentContainer1) as ViewGroup
+ val container2 =
+ activityRule.activity.findViewById<View>(R.id.fragmentContainer2) as ViewGroup
+ val fm = activityRule.activity.supportFragmentManager
+
+ val fragment1 = StrictViewFragment()
+ val fragment2 = StrictViewFragment()
+ val fragment3 = StrictViewFragment()
+ val fragment4 = StrictViewFragment()
+ val fragment5 = StrictViewFragment()
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer1, fragment1)
+ .add(R.id.fragmentContainer2, fragment2)
+ .replace(R.id.fragmentContainer1, fragment3)
+ .replace(R.id.fragmentContainer2, fragment4)
+ .replace(R.id.fragmentContainer1, fragment5)
+ .addToBackStack(null)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ assertChildren(container1, fragment5)
+ assertChildren(container2, fragment4)
+
+ fm.popBackStack()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ assertChildren(container1)
+ assertChildren(container2)
+ }
+
+ // Test to prevent regressions in FragmentManager fragment replace method. See b/24693644
+ @Test
+ fun testReplaceFragment() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val fm = activityRule.activity.supportFragmentManager
+ val fragmentA = StrictViewFragment(R.layout.text_a)
+
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragmentA)
+ .addToBackStack(null)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ assertThat(findViewById(R.id.textA)).isNotNull()
+ assertThat(findViewById(R.id.textB)).isNull()
+ assertThat(findViewById(R.id.textC)).isNull()
+
+ val fragmentB = StrictViewFragment(R.layout.text_b)
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragmentB)
+ .addToBackStack(null)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ assertThat(findViewById(R.id.textA)).isNotNull()
+ assertThat(findViewById(R.id.textB)).isNotNull()
+ assertThat(findViewById(R.id.textC)).isNull()
+
+ val fragmentC = StrictViewFragment(R.layout.text_c)
+ fm.beginTransaction()
+ .replace(R.id.fragmentContainer, fragmentC)
+ .addToBackStack(null)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ assertThat(findViewById(R.id.textA)).isNull()
+ assertThat(findViewById(R.id.textB)).isNull()
+ assertThat(findViewById(R.id.textC)).isNotNull()
+ }
+
+ // Test that adding a fragment with invisible or gone views does not end up with the view
+ // being visible
+ @Test
+ fun addInvisibleAndGoneFragments() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val container =
+ activityRule.activity.findViewById<View>(R.id.fragmentContainer) as ViewGroup
+ val fm = activityRule.activity.supportFragmentManager
+
+ val fragment1 = InvisibleFragment()
+ fm.beginTransaction().add(R.id.fragmentContainer, fragment1).addToBackStack(null).commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ FragmentTestUtil.assertChildren(container, fragment1)
+
+ assertThat(fragment1.requireView().visibility).isEqualTo(View.INVISIBLE)
+
+ val fragment2 = InvisibleFragment()
+ fragment2.visibility = View.GONE
+ fm.beginTransaction()
+ .replace(R.id.fragmentContainer, fragment2)
+ .addToBackStack(null)
+ .commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ FragmentTestUtil.assertChildren(container, fragment2)
+
+ assertThat(fragment2.requireView().visibility).isEqualTo(View.GONE)
+ }
+
+ // Test to ensure that popping and adding a fragment properly track the fragments added
+ // and removed.
+ @Test
+ fun popAdd() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val container =
+ activityRule.activity.findViewById<View>(R.id.fragmentContainer) as ViewGroup
+ val fm = activityRule.activity.supportFragmentManager
+
+ // One fragment with a view
+ val fragment1 = StrictViewFragment()
+ fm.beginTransaction().add(R.id.fragmentContainer, fragment1).addToBackStack(null).commit()
+ FragmentTestUtil.executePendingTransactions(activityRule)
+ FragmentTestUtil.assertChildren(container, fragment1)
+
+ val fragment2 = StrictViewFragment()
+ val fragment3 = StrictViewFragment()
+ instrumentation.runOnMainSync {
+ fm.popBackStack()
+ fm.beginTransaction()
+ .replace(R.id.fragmentContainer, fragment2)
+ .addToBackStack(null)
+ .commit()
+ fm.executePendingTransactions()
+ fm.popBackStack()
+ fm.beginTransaction()
+ .replace(R.id.fragmentContainer, fragment3)
+ .addToBackStack(null)
+ .commit()
+ fm.executePendingTransactions()
+ }
+ FragmentTestUtil.assertChildren(container, fragment3)
+ }
+
+ // Ensure that ordered transactions are executed individually rather than together.
+ // This forces references from one fragment to another that should be executed earlier
+ // to work.
+ @Test
+ fun orderedOperationsTogether() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val container =
+ activityRule.activity.findViewById<View>(R.id.fragmentContainer) as ViewGroup
+ val fm = activityRule.activity.supportFragmentManager
+
+ val fragment1 = StrictViewFragment(R.layout.scene1)
+ val fragment2 = StrictViewFragment(R.layout.fragment_a)
+
+ activityRule.runOnUiThread {
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment1)
+ .setReorderingAllowed(false)
+ .addToBackStack(null)
+ .commit()
+ fm.beginTransaction()
+ .add(R.id.squareContainer, fragment2)
+ .setReorderingAllowed(false)
+ .addToBackStack(null)
+ .commit()
+ fm.executePendingTransactions()
+ }
+ FragmentTestUtil.assertChildren(container, fragment1)
+ assertThat(findViewById(R.id.textA)).isNotNull()
+ }
+
+ // Ensure that there is no problem if the child fragment manager is used before
+ // the View has been added.
+ @Test
+ fun childFragmentManager() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val container =
+ activityRule.activity.findViewById<View>(R.id.fragmentContainer) as ViewGroup
+ val fm = activityRule.activity.supportFragmentManager
+
+ val fragment1 = ParentFragment()
+
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment1)
+ .addToBackStack(null)
+ .commit()
+
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ FragmentTestUtil.assertChildren(container, fragment1)
+ val innerContainer =
+ fragment1.requireView().findViewById<ViewGroup>(R.id.fragmentContainer1)
+
+ val fragment2 = fragment1.childFragmentManager.findFragmentByTag("inner")
+ FragmentTestUtil.assertChildren(innerContainer, fragment2)
+ }
+
+ // Popping the backstack with ordered fragments should execute the operations together.
+ // When a non-backstack fragment will be raised, it should not be destroyed.
+ @Test
+ fun popToNonBackStackFragment() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val fm = activityRule.activity.supportFragmentManager
+
+ val fragment1 = SimpleViewFragment()
+
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment1)
+ .commit()
+
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ val fragment2 = SimpleViewFragment()
+
+ fm.beginTransaction()
+ .replace(R.id.fragmentContainer, fragment2)
+ .addToBackStack("two")
+ .commit()
+
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ val fragment3 = SimpleViewFragment()
+
+ fm.beginTransaction()
+ .replace(R.id.fragmentContainer, fragment3)
+ .addToBackStack("three")
+ .commit()
+
+ FragmentTestUtil.executePendingTransactions(activityRule)
+
+ assertThat(fragment1.onCreateViewCount).isEqualTo(1)
+ assertThat(fragment2.onCreateViewCount).isEqualTo(1)
+ assertThat(fragment3.onCreateViewCount).isEqualTo(1)
+
+ FragmentTestUtil.popBackStackImmediate(
+ activityRule, "two",
+ FragmentManager.POP_BACK_STACK_INCLUSIVE
+ )
+
+ val container =
+ activityRule.activity.findViewById<View>(R.id.fragmentContainer) as ViewGroup
+
+ FragmentTestUtil.assertChildren(container, fragment1)
+
+ assertThat(fragment1.onCreateViewCount).isEqualTo(2)
+ assertThat(fragment2.onCreateViewCount).isEqualTo(1)
+ assertThat(fragment3.onCreateViewCount).isEqualTo(1)
+ }
+
+ private fun findViewById(viewId: Int): View? {
+ return activityRule.activity.findViewById(viewId)
+ }
+
+ private fun assertChildren(container: ViewGroup, vararg fragments: Fragment) {
+ val numFragments = fragments.size
+ assertWithMessage("There aren't the correct number of fragment Views in its container")
+ .that(container.childCount)
+ .isEqualTo(numFragments)
+ for (i in 0 until numFragments) {
+ assertWithMessage("Wrong Fragment View order for [$i]")
+ .that(fragments[i].view)
+ .isEqualTo(container.getChildAt(i))
+ }
+ }
+
+ class InvisibleFragment : StrictViewFragment() {
+ var visibility = View.INVISIBLE
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ view.visibility = visibility
+ super.onViewCreated(view, savedInstanceState)
+ }
+ }
+
+ class ParentFragment : StrictViewFragment(R.layout.double_container) {
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ val fragment2 = StrictViewFragment(R.layout.fragment_a)
+
+ childFragmentManager.beginTransaction()
+ .add(R.id.fragmentContainer1, fragment2, "inner")
+ .addToBackStack(null)
+ .commit()
+ childFragmentManager.executePendingTransactions()
+ return super.onCreateView(inflater, container, savedInstanceState)
+ }
+ }
+
+ @ContentView(R.layout.fragment_a)
+ class SimpleViewFragment : Fragment() {
+ var onCreateViewCount: Int = 0
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ onCreateViewCount++
+ return super.onCreateView(inflater, container, savedInstanceState)
+ }
+ }
+}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewTests.java b/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewTests.java
deleted file mode 100644
index 51a53c5..0000000
--- a/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewTests.java
+++ /dev/null
@@ -1,1069 +0,0 @@
-/*
- * 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 androidx.fragment.app;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.app.Instrumentation;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.ContentView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.test.FragmentTestActivity;
-import androidx.fragment.test.R;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.rule.ActivityTestRule;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class FragmentViewTests {
- @Rule
- public ActivityTestRule<FragmentTestActivity> mActivityRule =
- new ActivityTestRule<FragmentTestActivity>(FragmentTestActivity.class);
-
- private Instrumentation mInstrumentation;
-
- @Before
- public void setupInstrumentation() {
- mInstrumentation = InstrumentationRegistry.getInstrumentation();
- }
-
- // Test that adding a fragment adds the Views in the proper order. Popping the back stack
- // should remove the correct Views.
- @Test
- public void addFragments() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- ViewGroup container = (ViewGroup)
- mActivityRule.getActivity().findViewById(R.id.fragmentContainer);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
-
- // One fragment with a view
- final StrictViewFragment fragment1 = new StrictViewFragment();
- fm.beginTransaction().add(R.id.fragmentContainer, fragment1).addToBackStack(null).commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- FragmentTestUtil.assertChildren(container, fragment1);
-
- // Add another on top
- final StrictViewFragment fragment2 = new StrictViewFragment();
- fm.beginTransaction().add(R.id.fragmentContainer, fragment2).addToBackStack(null).commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- FragmentTestUtil.assertChildren(container, fragment1, fragment2);
-
- // Now add two in one transaction:
- final StrictViewFragment fragment3 = new StrictViewFragment();
- final StrictViewFragment fragment4 = new StrictViewFragment();
- fm.beginTransaction()
- .add(R.id.fragmentContainer, fragment3)
- .add(R.id.fragmentContainer, fragment4)
- .addToBackStack(null)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- FragmentTestUtil.assertChildren(container, fragment1, fragment2, fragment3, fragment4);
-
- fm.popBackStack();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- FragmentTestUtil.assertChildren(container, fragment1, fragment2);
-
- fm.popBackStack();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- assertEquals(1, container.getChildCount());
- FragmentTestUtil.assertChildren(container, fragment1);
-
- fm.popBackStack();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- FragmentTestUtil.assertChildren(container);
- }
-
- // Add fragments to multiple containers in the same transaction. Make sure that
- // they pop correctly, too.
- @Test
- public void addTwoContainers() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.double_container);
- ViewGroup container1 = (ViewGroup)
- mActivityRule.getActivity().findViewById(R.id.fragmentContainer1);
- ViewGroup container2 = (ViewGroup)
- mActivityRule.getActivity().findViewById(R.id.fragmentContainer2);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
-
- final StrictViewFragment fragment1 = new StrictViewFragment();
- fm.beginTransaction().add(R.id.fragmentContainer1, fragment1).addToBackStack(null).commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- FragmentTestUtil.assertChildren(container1, fragment1);
-
- final StrictViewFragment fragment2 = new StrictViewFragment();
- fm.beginTransaction().add(R.id.fragmentContainer2, fragment2).addToBackStack(null).commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- FragmentTestUtil.assertChildren(container2, fragment2);
-
- final StrictViewFragment fragment3 = new StrictViewFragment();
- final StrictViewFragment fragment4 = new StrictViewFragment();
- fm.beginTransaction()
- .add(R.id.fragmentContainer1, fragment3)
- .add(R.id.fragmentContainer2, fragment4)
- .addToBackStack(null)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container1, fragment1, fragment3);
- FragmentTestUtil.assertChildren(container2, fragment2, fragment4);
-
- fm.popBackStack();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- FragmentTestUtil.assertChildren(container1, fragment1);
- FragmentTestUtil.assertChildren(container2, fragment2);
-
- fm.popBackStack();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- FragmentTestUtil.assertChildren(container1, fragment1);
- FragmentTestUtil.assertChildren(container2);
-
- fm.popBackStack();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- assertEquals(0, container1.getChildCount());
- }
-
- // When you add a fragment that's has already been added, it should throw.
- @Test
- public void doubleAdd() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictViewFragment fragment1 = new StrictViewFragment();
- fm.beginTransaction().add(R.id.fragmentContainer, fragment1).commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- mInstrumentation.runOnMainSync(new Runnable() {
- @Override
- public void run() {
- try {
- fm.beginTransaction()
- .add(R.id.fragmentContainer, fragment1)
- .addToBackStack(null)
- .commit();
- fm.executePendingTransactions();
- fail("Adding a fragment that is already added should be an error");
- } catch (IllegalStateException e) {
- // expected
- }
- }
- });
- }
-
- // Make sure that removed fragments remove the right Views. Popping the back stack should
- // add the Views back properly
- @Test
- public void removeFragments() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- ViewGroup container = (ViewGroup)
- mActivityRule.getActivity().findViewById(R.id.fragmentContainer);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictViewFragment fragment1 = new StrictViewFragment();
- final StrictViewFragment fragment2 = new StrictViewFragment();
- final StrictViewFragment fragment3 = new StrictViewFragment();
- final StrictViewFragment fragment4 = new StrictViewFragment();
- fm.beginTransaction()
- .add(R.id.fragmentContainer, fragment1, "1")
- .add(R.id.fragmentContainer, fragment2, "2")
- .add(R.id.fragmentContainer, fragment3, "3")
- .add(R.id.fragmentContainer, fragment4, "4")
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- FragmentTestUtil.assertChildren(container, fragment1, fragment2, fragment3, fragment4);
-
- // Remove a view
- fm.beginTransaction().remove(fragment4).addToBackStack(null).commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- assertEquals(3, container.getChildCount());
- FragmentTestUtil.assertChildren(container, fragment1, fragment2, fragment3);
-
- // remove another one
- fm.beginTransaction().remove(fragment2).addToBackStack(null).commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- FragmentTestUtil.assertChildren(container, fragment1, fragment3);
-
- // Now remove the remaining:
- fm.beginTransaction()
- .remove(fragment3)
- .remove(fragment1)
- .addToBackStack(null)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- FragmentTestUtil.assertChildren(container);
-
- fm.popBackStack();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- final Fragment replacement1 = fm.findFragmentByTag("1");
- final Fragment replacement3 = fm.findFragmentByTag("3");
- FragmentTestUtil.assertChildren(container, replacement1, replacement3);
-
- fm.popBackStack();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- final Fragment replacement2 = fm.findFragmentByTag("2");
- FragmentTestUtil.assertChildren(container, replacement1, replacement3, replacement2);
-
- fm.popBackStack();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- final Fragment replacement4 = fm.findFragmentByTag("4");
- FragmentTestUtil.assertChildren(container, replacement1, replacement3, replacement2,
- replacement4);
- }
-
- // Removing a hidden fragment should remove the View and popping should bring it back hidden
- @Test
- public void removeHiddenView() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- ViewGroup container = (ViewGroup)
- mActivityRule.getActivity().findViewById(R.id.fragmentContainer);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictViewFragment fragment1 = new StrictViewFragment();
- fm.beginTransaction().add(R.id.fragmentContainer, fragment1, "1").hide(fragment1).commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- FragmentTestUtil.assertChildren(container, fragment1);
- assertTrue(fragment1.isHidden());
-
- fm.beginTransaction().remove(fragment1).addToBackStack(null).commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- FragmentTestUtil.assertChildren(container);
-
- fm.popBackStack();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- final Fragment replacement1 = fm.findFragmentByTag("1");
- FragmentTestUtil.assertChildren(container, replacement1);
- assertTrue(replacement1.isHidden());
- assertEquals(View.GONE, replacement1.requireView().getVisibility());
- }
-
- // Removing a detached fragment should do nothing to the View and popping should bring
- // the Fragment back detached
- @Test
- public void removeDetatchedView() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- ViewGroup container = (ViewGroup)
- mActivityRule.getActivity().findViewById(R.id.fragmentContainer);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictViewFragment fragment1 = new StrictViewFragment();
- fm.beginTransaction()
- .add(R.id.fragmentContainer, fragment1, "1")
- .detach(fragment1)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- FragmentTestUtil.assertChildren(container);
- assertTrue(fragment1.isDetached());
-
- fm.beginTransaction().remove(fragment1).addToBackStack(null).commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- FragmentTestUtil.assertChildren(container);
-
- fm.popBackStack();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- final Fragment replacement1 = fm.findFragmentByTag("1");
- FragmentTestUtil.assertChildren(container);
- assertTrue(replacement1.isDetached());
- }
-
- // Unlike adding the same fragment twice, you should be able to add and then remove and then
- // add the same fragment in one transaction.
- @Test
- public void addRemoveAdd() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- ViewGroup container = (ViewGroup)
- mActivityRule.getActivity().findViewById(R.id.fragmentContainer);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictViewFragment fragment = new StrictViewFragment();
- fm.beginTransaction()
- .add(R.id.fragmentContainer, fragment)
- .remove(fragment)
- .add(R.id.fragmentContainer, fragment)
- .addToBackStack(null)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- FragmentTestUtil.assertChildren(container, fragment);
-
- fm.popBackStack();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- FragmentTestUtil.assertChildren(container);
- }
-
- // Removing a fragment that isn't in should not throw
- @Test
- public void removeNothThere() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictViewFragment fragment = new StrictViewFragment();
- fm.beginTransaction().remove(fragment).commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- }
-
- // Hide a fragment and its View should be GONE. Then pop it and the View should be VISIBLE
- @Test
- public void hideFragment() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- ViewGroup container = (ViewGroup)
- mActivityRule.getActivity().findViewById(R.id.fragmentContainer);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictViewFragment fragment = new StrictViewFragment();
- fm.beginTransaction().add(R.id.fragmentContainer, fragment).commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container, fragment);
- assertEquals(View.VISIBLE, fragment.requireView().getVisibility());
-
- fm.beginTransaction().hide(fragment).addToBackStack(null).commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container, fragment);
- assertTrue(fragment.isHidden());
- assertEquals(View.GONE, fragment.requireView().getVisibility());
-
- fm.popBackStack();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container, fragment);
- assertFalse(fragment.isHidden());
- assertEquals(View.VISIBLE, fragment.requireView().getVisibility());
- }
-
- // Hiding a hidden fragment should not throw
- @Test
- public void doubleHide() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictViewFragment fragment = new StrictViewFragment();
- fm.beginTransaction()
- .add(R.id.fragmentContainer, fragment)
- .hide(fragment)
- .hide(fragment)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- }
-
- // Hiding a non-existing fragment should not throw
- @Test
- public void hideUnAdded() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictViewFragment fragment = new StrictViewFragment();
- fm.beginTransaction()
- .hide(fragment)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- }
-
- // Show a hidden fragment and its View should be VISIBLE. Then pop it and the View should be
- // GONE.
- @Test
- public void showFragment() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- ViewGroup container = (ViewGroup)
- mActivityRule.getActivity().findViewById(R.id.fragmentContainer);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictViewFragment fragment = new StrictViewFragment();
- fm.beginTransaction().add(R.id.fragmentContainer, fragment).hide(fragment).commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container, fragment);
- assertTrue(fragment.isHidden());
- assertEquals(View.GONE, fragment.requireView().getVisibility());
-
- fm.beginTransaction().show(fragment).addToBackStack(null).commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container, fragment);
- assertFalse(fragment.isHidden());
- assertEquals(View.VISIBLE, fragment.requireView().getVisibility());
-
- fm.popBackStack();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container, fragment);
- assertTrue(fragment.isHidden());
- assertEquals(View.GONE, fragment.requireView().getVisibility());
- }
-
- // Showing a shown fragment should not throw
- @Test
- public void showShown() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictViewFragment fragment = new StrictViewFragment();
- fm.beginTransaction()
- .add(R.id.fragmentContainer, fragment)
- .show(fragment)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- }
-
- // Showing a non-existing fragment should not throw
- @Test
- public void showUnAdded() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictViewFragment fragment = new StrictViewFragment();
- fm.beginTransaction()
- .show(fragment)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- }
-
- // Detaching a fragment should remove the View from the hierarchy. Then popping it should
- // bring it back VISIBLE
- @Test
- public void detachFragment() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- ViewGroup container = (ViewGroup)
- mActivityRule.getActivity().findViewById(R.id.fragmentContainer);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictViewFragment fragment = new StrictViewFragment();
- fm.beginTransaction().add(R.id.fragmentContainer, fragment).commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container, fragment);
- assertFalse(fragment.isDetached());
- assertEquals(View.VISIBLE, fragment.requireView().getVisibility());
-
- fm.beginTransaction().detach(fragment).addToBackStack(null).commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container);
- assertTrue(fragment.isDetached());
-
- fm.popBackStack();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container, fragment);
- assertFalse(fragment.isDetached());
- assertEquals(View.VISIBLE, fragment.requireView().getVisibility());
- }
-
- // Detaching a hidden fragment should remove the View from the hierarchy. Then popping it should
- // bring it back hidden
- @Test
- public void detachHiddenFragment() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- ViewGroup container = (ViewGroup)
- mActivityRule.getActivity().findViewById(R.id.fragmentContainer);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictViewFragment fragment = new StrictViewFragment();
- fm.beginTransaction().add(R.id.fragmentContainer, fragment).hide(fragment).commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container, fragment);
- assertFalse(fragment.isDetached());
- assertTrue(fragment.isHidden());
- assertEquals(View.GONE, fragment.requireView().getVisibility());
-
- fm.beginTransaction().detach(fragment).addToBackStack(null).commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container);
- assertTrue(fragment.isHidden());
- assertTrue(fragment.isDetached());
-
- fm.popBackStack();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container, fragment);
- assertTrue(fragment.isHidden());
- assertFalse(fragment.isDetached());
- assertEquals(View.GONE, fragment.requireView().getVisibility());
- }
-
- // Detaching a detached fragment should not throw
- @Test
- public void detachDetatched() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictViewFragment fragment = new StrictViewFragment();
- fm.beginTransaction()
- .add(R.id.fragmentContainer, fragment)
- .detach(fragment)
- .detach(fragment)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- }
-
- // Detaching a non-existing fragment should not throw
- @Test
- public void detachUnAdded() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictViewFragment fragment = new StrictViewFragment();
- fm.beginTransaction()
- .detach(fragment)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- }
-
- // Attaching a fragment should add the View back into the hierarchy. Then popping it should
- // remove it again
- @Test
- public void attachFragment() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- ViewGroup container = (ViewGroup)
- mActivityRule.getActivity().findViewById(R.id.fragmentContainer);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictViewFragment fragment = new StrictViewFragment();
- fm.beginTransaction().add(R.id.fragmentContainer, fragment).detach(fragment).commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container);
- assertTrue(fragment.isDetached());
-
- fm.beginTransaction().attach(fragment).addToBackStack(null).commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container, fragment);
- assertFalse(fragment.isDetached());
- assertEquals(View.VISIBLE, fragment.requireView().getVisibility());
-
- fm.popBackStack();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container);
- assertTrue(fragment.isDetached());
- }
-
- // Attaching a hidden fragment should add the View as GONE the hierarchy. Then popping it should
- // remove it again.
- @Test
- public void attachHiddenFragment() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- ViewGroup container = (ViewGroup)
- mActivityRule.getActivity().findViewById(R.id.fragmentContainer);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictViewFragment fragment = new StrictViewFragment();
- fm.beginTransaction()
- .add(R.id.fragmentContainer, fragment)
- .hide(fragment)
- .detach(fragment)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container);
- assertTrue(fragment.isDetached());
- assertTrue(fragment.isHidden());
-
- fm.beginTransaction().attach(fragment).addToBackStack(null).commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container, fragment);
- assertTrue(fragment.isHidden());
- assertFalse(fragment.isDetached());
- assertEquals(View.GONE, fragment.requireView().getVisibility());
-
- fm.popBackStack();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container);
- assertTrue(fragment.isDetached());
- assertTrue(fragment.isHidden());
- }
-
- // Attaching an attached fragment should not throw
- @Test
- public void attachAttached() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictViewFragment fragment = new StrictViewFragment();
- fm.beginTransaction()
- .add(R.id.fragmentContainer, fragment)
- .attach(fragment)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- }
-
- // Attaching a non-existing fragment should not throw
- @Test
- public void attachUnAdded() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictViewFragment fragment = new StrictViewFragment();
- fm.beginTransaction()
- .attach(fragment)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- }
-
- // Simple replace of one fragment in a container. Popping should replace it back again
- @Test
- public void replaceOne() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- ViewGroup container = (ViewGroup)
- mActivityRule.getActivity().findViewById(R.id.fragmentContainer);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictViewFragment fragment1 = new StrictViewFragment();
- fm.beginTransaction().add(R.id.fragmentContainer, fragment1, "1").commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container, fragment1);
-
- final StrictViewFragment fragment2 = new StrictViewFragment();
- fm.beginTransaction()
- .replace(R.id.fragmentContainer, fragment2)
- .addToBackStack(null)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container, fragment2);
- assertEquals(View.VISIBLE, fragment2.requireView().getVisibility());
-
- fm.popBackStack();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- Fragment replacement1 = fm.findFragmentByTag("1");
- assertNotNull(replacement1);
- FragmentTestUtil.assertChildren(container, replacement1);
- assertFalse(replacement1.isHidden());
- assertTrue(replacement1.isAdded());
- assertFalse(replacement1.isDetached());
- assertEquals(View.VISIBLE, replacement1.requireView().getVisibility());
- }
-
- // Replace of multiple fragments in a container. Popping should replace it back again
- @Test
- public void replaceTwo() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- ViewGroup container = (ViewGroup)
- mActivityRule.getActivity().findViewById(R.id.fragmentContainer);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictViewFragment fragment1 = new StrictViewFragment();
- final StrictViewFragment fragment2 = new StrictViewFragment();
- fm.beginTransaction()
- .add(R.id.fragmentContainer, fragment1, "1")
- .add(R.id.fragmentContainer, fragment2, "2")
- .hide(fragment2)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container, fragment1, fragment2);
-
- final StrictViewFragment fragment3 = new StrictViewFragment();
- fm.beginTransaction()
- .replace(R.id.fragmentContainer, fragment3)
- .addToBackStack(null)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container, fragment3);
- assertEquals(View.VISIBLE, fragment3.requireView().getVisibility());
-
- fm.popBackStack();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- Fragment replacement1 = fm.findFragmentByTag("1");
- Fragment replacement2 = fm.findFragmentByTag("2");
- assertNotNull(replacement1);
- assertNotNull(replacement2);
- FragmentTestUtil.assertChildren(container, replacement1, replacement2);
- assertFalse(replacement1.isHidden());
- assertTrue(replacement1.isAdded());
- assertFalse(replacement1.isDetached());
- assertEquals(View.VISIBLE, replacement1.requireView().getVisibility());
-
- // fragment2 was hidden, so it should be returned hidden
- assertTrue(replacement2.isHidden());
- assertTrue(replacement2.isAdded());
- assertFalse(replacement2.isDetached());
- assertEquals(View.GONE, replacement2.requireView().getVisibility());
- }
-
- // Replace of empty container. Should act as add and popping should just remove the fragment
- @Test
- public void replaceZero() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- ViewGroup container = (ViewGroup)
- mActivityRule.getActivity().findViewById(R.id.fragmentContainer);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
-
- final StrictViewFragment fragment = new StrictViewFragment();
- fm.beginTransaction()
- .replace(R.id.fragmentContainer, fragment)
- .addToBackStack(null)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container, fragment);
- assertEquals(View.VISIBLE, fragment.requireView().getVisibility());
-
- fm.popBackStack();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container);
- }
-
- // Replace a fragment that exists with itself
- @Test
- public void replaceExisting() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- ViewGroup container = (ViewGroup)
- mActivityRule.getActivity().findViewById(R.id.fragmentContainer);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictViewFragment fragment1 = new StrictViewFragment();
- final StrictViewFragment fragment2 = new StrictViewFragment();
- fm.beginTransaction()
- .add(R.id.fragmentContainer, fragment1, "1")
- .add(R.id.fragmentContainer, fragment2, "2")
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container, fragment1, fragment2);
-
- fm.beginTransaction()
- .replace(R.id.fragmentContainer, fragment1)
- .addToBackStack(null)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container, fragment1);
-
- fm.popBackStack();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- final Fragment replacement1 = fm.findFragmentByTag("1");
- final Fragment replacement2 = fm.findFragmentByTag("2");
-
- assertSame(fragment1, replacement1);
- FragmentTestUtil.assertChildren(container, replacement1, replacement2);
- }
-
- // Have two replace operations in the same transaction to ensure that they
- // don't interfere with each other
- @Test
- public void replaceReplace() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.double_container);
- ViewGroup container1 = (ViewGroup)
- mActivityRule.getActivity().findViewById(R.id.fragmentContainer1);
- ViewGroup container2 = (ViewGroup)
- mActivityRule.getActivity().findViewById(R.id.fragmentContainer2);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
-
- final StrictViewFragment fragment1 = new StrictViewFragment();
- final StrictViewFragment fragment2 = new StrictViewFragment();
- final StrictViewFragment fragment3 = new StrictViewFragment();
- final StrictViewFragment fragment4 = new StrictViewFragment();
- final StrictViewFragment fragment5 = new StrictViewFragment();
- fm.beginTransaction()
- .add(R.id.fragmentContainer1, fragment1)
- .add(R.id.fragmentContainer2, fragment2)
- .replace(R.id.fragmentContainer1, fragment3)
- .replace(R.id.fragmentContainer2, fragment4)
- .replace(R.id.fragmentContainer1, fragment5)
- .addToBackStack(null)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- assertChildren(container1, fragment5);
- assertChildren(container2, fragment4);
-
- fm.popBackStack();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- assertChildren(container1);
- assertChildren(container2);
- }
-
- // Test to prevent regressions in FragmentManager fragment replace method. See b/24693644
- @Test
- public void testReplaceFragment() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- StrictViewFragment fragmentA = new StrictViewFragment();
- fragmentA.setLayoutId(R.layout.text_a);
-
- fm.beginTransaction()
- .add(R.id.fragmentContainer, fragmentA)
- .addToBackStack(null)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- assertNotNull(findViewById(R.id.textA));
- assertNull(findViewById(R.id.textB));
- assertNull(findViewById(R.id.textC));
-
- StrictViewFragment fragmentB = new StrictViewFragment();
- fragmentB.setLayoutId(R.layout.text_b);
- fm.beginTransaction()
- .add(R.id.fragmentContainer, fragmentB)
- .addToBackStack(null)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- assertNotNull(findViewById(R.id.textA));
- assertNotNull(findViewById(R.id.textB));
- assertNull(findViewById(R.id.textC));
-
- StrictViewFragment fragmentC = new StrictViewFragment();
- fragmentC.setLayoutId(R.layout.text_c);
- fm.beginTransaction()
- .replace(R.id.fragmentContainer, fragmentC)
- .addToBackStack(null)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- assertNull(findViewById(R.id.textA));
- assertNull(findViewById(R.id.textB));
- assertNotNull(findViewById(R.id.textC));
- }
-
- // Test that adding a fragment with invisible or gone views does not end up with the view
- // being visible
- @Test
- public void addInvisibleAndGoneFragments() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- ViewGroup container = (ViewGroup)
- mActivityRule.getActivity().findViewById(R.id.fragmentContainer);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
-
- final StrictViewFragment fragment1 = new InvisibleFragment();
- fm.beginTransaction().add(R.id.fragmentContainer, fragment1).addToBackStack(null).commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- FragmentTestUtil.assertChildren(container, fragment1);
-
- assertEquals(View.INVISIBLE, fragment1.requireView().getVisibility());
-
- final InvisibleFragment fragment2 = new InvisibleFragment();
- fragment2.visibility = View.GONE;
- fm.beginTransaction()
- .replace(R.id.fragmentContainer, fragment2)
- .addToBackStack(null)
- .commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- FragmentTestUtil.assertChildren(container, fragment2);
-
- assertEquals(View.GONE, fragment2.requireView().getVisibility());
- }
-
- // Test to ensure that popping and adding a fragment properly track the fragments added
- // and removed.
- @Test
- public void popAdd() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- ViewGroup container = (ViewGroup)
- mActivityRule.getActivity().findViewById(R.id.fragmentContainer);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
-
- // One fragment with a view
- final StrictViewFragment fragment1 = new StrictViewFragment();
- fm.beginTransaction().add(R.id.fragmentContainer, fragment1).addToBackStack(null).commit();
- FragmentTestUtil.executePendingTransactions(mActivityRule);
- FragmentTestUtil.assertChildren(container, fragment1);
-
- final StrictViewFragment fragment2 = new StrictViewFragment();
- final StrictViewFragment fragment3 = new StrictViewFragment();
- mInstrumentation.runOnMainSync(new Runnable() {
- @Override
- public void run() {
- fm.popBackStack();
- fm.beginTransaction()
- .replace(R.id.fragmentContainer, fragment2)
- .addToBackStack(null)
- .commit();
- fm.executePendingTransactions();
- fm.popBackStack();
- fm.beginTransaction()
- .replace(R.id.fragmentContainer, fragment3)
- .addToBackStack(null)
- .commit();
- fm.executePendingTransactions();
- }
- });
- FragmentTestUtil.assertChildren(container, fragment3);
- }
-
- // Ensure that ordered transactions are executed individually rather than together.
- // This forces references from one fragment to another that should be executed earlier
- // to work.
- @Test
- public void orderedOperationsTogether() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- ViewGroup container = (ViewGroup)
- mActivityRule.getActivity().findViewById(R.id.fragmentContainer);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
-
- final StrictViewFragment fragment1 = new StrictViewFragment();
- fragment1.setLayoutId(R.layout.scene1);
- final StrictViewFragment fragment2 = new StrictViewFragment();
- fragment2.setLayoutId(R.layout.fragment_a);
-
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- fm.beginTransaction()
- .add(R.id.fragmentContainer, fragment1)
- .setReorderingAllowed(false)
- .addToBackStack(null)
- .commit();
- fm.beginTransaction()
- .add(R.id.squareContainer, fragment2)
- .setReorderingAllowed(false)
- .addToBackStack(null)
- .commit();
- fm.executePendingTransactions();
- }
- });
- FragmentTestUtil.assertChildren(container, fragment1);
- assertNotNull(findViewById(R.id.textA));
- }
-
- // Ensure that there is no problem if the child fragment manager is used before
- // the View has been added.
- @Test
- public void childFragmentManager() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- ViewGroup container = (ViewGroup)
- mActivityRule.getActivity().findViewById(R.id.fragmentContainer);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
-
- final StrictViewFragment fragment1 = new ParentFragment();
- fragment1.setLayoutId(R.layout.double_container);
-
- fm.beginTransaction()
- .add(R.id.fragmentContainer, fragment1)
- .addToBackStack(null)
- .commit();
-
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- FragmentTestUtil.assertChildren(container, fragment1);
- ViewGroup innerContainer = fragment1.requireView().findViewById(R.id.fragmentContainer1);
-
- Fragment fragment2 = fragment1.getChildFragmentManager().findFragmentByTag("inner");
- FragmentTestUtil.assertChildren(innerContainer, fragment2);
- }
-
- // Popping the backstack with ordered fragments should execute the operations together.
- // When a non-backstack fragment will be raised, it should not be destroyed.
- @Test
- public void popToNonBackStackFragment() throws Throwable {
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
-
- final SimpleViewFragment fragment1 = new SimpleViewFragment();
-
- fm.beginTransaction()
- .add(R.id.fragmentContainer, fragment1)
- .commit();
-
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- final SimpleViewFragment fragment2 = new SimpleViewFragment();
-
- fm.beginTransaction()
- .replace(R.id.fragmentContainer, fragment2)
- .addToBackStack("two")
- .commit();
-
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- final SimpleViewFragment fragment3 = new SimpleViewFragment();
-
- fm.beginTransaction()
- .replace(R.id.fragmentContainer, fragment3)
- .addToBackStack("three")
- .commit();
-
- FragmentTestUtil.executePendingTransactions(mActivityRule);
-
- assertEquals(1, fragment1.onCreateViewCount);
- assertEquals(1, fragment2.onCreateViewCount);
- assertEquals(1, fragment3.onCreateViewCount);
-
- FragmentTestUtil.popBackStackImmediate(mActivityRule, "two",
- FragmentManager.POP_BACK_STACK_INCLUSIVE);
-
- ViewGroup container = (ViewGroup)
- mActivityRule.getActivity().findViewById(R.id.fragmentContainer);
-
- FragmentTestUtil.assertChildren(container, fragment1);
-
- assertEquals(2, fragment1.onCreateViewCount);
- assertEquals(1, fragment2.onCreateViewCount);
- assertEquals(1, fragment3.onCreateViewCount);
- }
-
- private View findViewById(int viewId) {
- return mActivityRule.getActivity().findViewById(viewId);
- }
-
- private void assertChildren(ViewGroup container, Fragment... fragments) {
- final int numFragments = fragments == null ? 0 : fragments.length;
- assertEquals("There aren't the correct number of fragment Views in its container",
- numFragments, container.getChildCount());
- for (int i = 0; i < numFragments; i++) {
- assertEquals("Wrong Fragment View order for [" + i + "]", container.getChildAt(i),
- fragments[i].getView());
- }
- }
-
- public static class InvisibleFragment extends StrictViewFragment {
- public int visibility = View.INVISIBLE;
-
- @Override
- public void onViewCreated(View view, Bundle savedInstanceState) {
- view.setVisibility(visibility);
- super.onViewCreated(view, savedInstanceState);
- }
- }
-
- public static class ParentFragment extends StrictViewFragment {
- public ParentFragment() {
- setLayoutId(R.layout.double_container);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- View view = super.onCreateView(inflater, container, savedInstanceState);
- final StrictViewFragment fragment2 = new StrictViewFragment();
- fragment2.setLayoutId(R.layout.fragment_a);
-
- getChildFragmentManager().beginTransaction()
- .add(R.id.fragmentContainer1, fragment2, "inner")
- .addToBackStack(null)
- .commit();
- getChildFragmentManager().executePendingTransactions();
- return view;
- }
- }
-
- @ContentView(R.layout.fragment_a)
- public static class SimpleViewFragment extends Fragment {
- public int onCreateViewCount;
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- onCreateViewCount++;
- return super.onCreateView(inflater, container, savedInstanceState);
- }
- }
-}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/LoaderTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/LoaderTest.kt
index 7c17551..4242a3b 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/LoaderTest.kt
+++ b/fragment/src/androidTest/java/androidx/fragment/app/LoaderTest.kt
@@ -64,7 +64,7 @@
FragmentTestUtil.executePendingTransactions(activityRule, fm)
- val weakActivity = WeakReference(LoaderActivity.sActivity)
+ val weakActivity = WeakReference(LoaderActivity.activity)
// Wait for everything to settle. We have to make sure that the old Activity
// is ready to be collected.
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/NestedFragmentRestoreTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/NestedFragmentRestoreTest.kt
index 7000783..ec335ed 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/NestedFragmentRestoreTest.kt
+++ b/fragment/src/androidTest/java/androidx/fragment/app/NestedFragmentRestoreTest.kt
@@ -41,7 +41,7 @@
val activity = activityRule.activity
activityRule.runOnUiThread {
val parent = ParentFragment()
- parent.setRetainChildInstance(true)
+ parent.retainChildInstance = true
activity.supportFragmentManager.beginTransaction()
.add(parent, "parent")
@@ -54,7 +54,7 @@
var attachedTo: Context? = null
val latch = CountDownLatch(1)
- child.setOnAttachListener { context, _ ->
+ child.onAttachListener = { context ->
attachedTo = context
latch.countDown()
}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/PostponedTransitionTest.java b/fragment/src/androidTest/java/androidx/fragment/app/PostponedTransitionTest.java
deleted file mode 100644
index 114461e..0000000
--- a/fragment/src/androidTest/java/androidx/fragment/app/PostponedTransitionTest.java
+++ /dev/null
@@ -1,987 +0,0 @@
-/*
- * 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 androidx.fragment.app;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.app.Instrumentation;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Parcelable;
-import android.util.Pair;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.ContentView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.test.FragmentTestActivity;
-import androidx.fragment.test.R;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.LargeTest;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.rule.ActivityTestRule;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.CountDownLatch;
-
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
-public class PostponedTransitionTest {
- @Rule
- public ActivityTestRule<FragmentTestActivity> mActivityRule =
- new ActivityTestRule<FragmentTestActivity>(FragmentTestActivity.class);
-
- private Instrumentation mInstrumentation;
- private PostponedFragment1 mBeginningFragment;
-
- @Before
- public void setupContainer() throws Throwable {
- mInstrumentation = InstrumentationRegistry.getInstrumentation();
- FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- mBeginningFragment = new PostponedFragment1();
-
- final CountDownLatch backStackLatch = new CountDownLatch(1);
- FragmentManager.OnBackStackChangedListener backstackListener =
- new FragmentManager.OnBackStackChangedListener() {
-
- @Override
- public void onBackStackChanged() {
- backStackLatch.countDown();
- fm.removeOnBackStackChangedListener(this);
- }
- };
- fm.addOnBackStackChangedListener(backstackListener);
- fm.beginTransaction()
- .add(R.id.fragmentContainer, mBeginningFragment)
- .setReorderingAllowed(true)
- .addToBackStack(null)
- .commit();
-
- backStackLatch.await();
-
- mBeginningFragment.startPostponedEnterTransition();
- mBeginningFragment.waitForTransition();
- clearTargets(mBeginningFragment);
- }
-
- // Ensure that replacing with a fragment that has a postponed transition
- // will properly postpone it, both adding and popping.
- @Test
- public void replaceTransition() throws Throwable {
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final View startBlue = mActivityRule.getActivity().findViewById(R.id.blueSquare);
-
- final PostponedFragment2 fragment = new PostponedFragment2();
- fm.beginTransaction()
- .addSharedElement(startBlue, "blueSquare")
- .replace(R.id.fragmentContainer, fragment)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
-
- FragmentTestUtil.waitForExecution(mActivityRule);
-
- // should be postponed now
- assertPostponedTransition(mBeginningFragment, fragment, null);
-
- // start the postponed transition
- fragment.startPostponedEnterTransition();
-
- // make sure it ran
- assertForwardTransition(mBeginningFragment, fragment);
-
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
-
- // should be postponed going back, too
- assertPostponedTransition(fragment, mBeginningFragment, null);
-
- // start the postponed transition
- mBeginningFragment.startPostponedEnterTransition();
-
- // make sure it ran
- assertBackTransition(fragment, mBeginningFragment);
- }
-
- // Ensure that replacing a fragment doesn't cause problems with the back stack nesting level
- @Test
- public void backStackNestingLevel() throws Throwable {
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- View startBlue = mActivityRule.getActivity().findViewById(R.id.blueSquare);
-
- final TransitionFragment fragment1 = new TransitionFragment2();
- fm.beginTransaction()
- .addSharedElement(startBlue, "blueSquare")
- .replace(R.id.fragmentContainer, fragment1)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
-
- // make sure transition ran
- assertForwardTransition(mBeginningFragment, fragment1);
-
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
-
- // should be postponed going back
- assertPostponedTransition(fragment1, mBeginningFragment, null);
-
- // start the postponed transition
- mBeginningFragment.startPostponedEnterTransition();
-
- // make sure it ran
- assertBackTransition(fragment1, mBeginningFragment);
-
- startBlue = mActivityRule.getActivity().findViewById(R.id.blueSquare);
-
- final TransitionFragment fragment2 = new TransitionFragment2();
- fm.beginTransaction()
- .addSharedElement(startBlue, "blueSquare")
- .replace(R.id.fragmentContainer, fragment2)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
-
- // make sure transition ran
- assertForwardTransition(mBeginningFragment, fragment2);
-
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
-
- // should be postponed going back
- assertPostponedTransition(fragment2, mBeginningFragment, null);
-
- // start the postponed transition
- mBeginningFragment.startPostponedEnterTransition();
-
- // make sure it ran
- assertBackTransition(fragment2, mBeginningFragment);
- }
-
- // Ensure that postponed transition is forced after another has been committed.
- // This tests when the transactions are executed together
- @Test
- public void forcedTransition1() throws Throwable {
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final View startBlue = mActivityRule.getActivity().findViewById(R.id.blueSquare);
-
- final PostponedFragment2 fragment2 = new PostponedFragment2();
- final PostponedFragment1 fragment3 = new PostponedFragment1();
-
- final int[] commit = new int[1];
- // Need to run this on the UI thread so that the transaction doesn't start
- // between the two
- mInstrumentation.runOnMainSync(new Runnable() {
- @Override
- public void run() {
- commit[0] = fm.beginTransaction()
- .addSharedElement(startBlue, "blueSquare")
- .replace(R.id.fragmentContainer, fragment2)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
-
- fm.beginTransaction()
- .addSharedElement(startBlue, "blueSquare")
- .replace(R.id.fragmentContainer, fragment3)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- }
- });
- FragmentTestUtil.waitForExecution(mActivityRule);
-
- // transition to fragment2 should be started
- assertForwardTransition(mBeginningFragment, fragment2);
-
- // fragment3 should be postponed, but fragment2 should be executed with no transition.
- assertPostponedTransition(fragment2, fragment3, mBeginningFragment);
-
- // start the postponed transition
- fragment3.startPostponedEnterTransition();
-
- // make sure it ran
- assertForwardTransition(fragment2, fragment3);
-
- FragmentTestUtil.popBackStackImmediate(mActivityRule, commit[0],
- FragmentManager.POP_BACK_STACK_INCLUSIVE);
-
- assertBackTransition(fragment3, fragment2);
-
- assertPostponedTransition(fragment2, mBeginningFragment, fragment3);
-
- // start the postponed transition
- mBeginningFragment.startPostponedEnterTransition();
-
- // make sure it ran
- assertBackTransition(fragment2, mBeginningFragment);
- }
-
- // Ensure that postponed transition is forced after another has been committed.
- // This tests when the transactions are processed separately.
- @Test
- public void forcedTransition2() throws Throwable {
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final View startBlue = mActivityRule.getActivity().findViewById(R.id.blueSquare);
-
- final PostponedFragment2 fragment2 = new PostponedFragment2();
-
- fm.beginTransaction()
- .addSharedElement(startBlue, "blueSquare")
- .replace(R.id.fragmentContainer, fragment2)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
-
- FragmentTestUtil.waitForExecution(mActivityRule);
-
- assertPostponedTransition(mBeginningFragment, fragment2, null);
-
- final PostponedFragment1 fragment3 = new PostponedFragment1();
- fm.beginTransaction()
- .addSharedElement(startBlue, "blueSquare")
- .replace(R.id.fragmentContainer, fragment3)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
-
- // This should cancel the mBeginningFragment -> fragment2 transition
- // and start fragment2 -> fragment3 transition postponed
- FragmentTestUtil.waitForExecution(mActivityRule);
-
- // fragment3 should be postponed, but fragment2 should be executed with no transition.
- assertPostponedTransition(fragment2, fragment3, mBeginningFragment);
-
- // start the postponed transition
- fragment3.startPostponedEnterTransition();
-
- // make sure it ran
- assertForwardTransition(fragment2, fragment3);
-
- // Pop back to fragment2, but it should be postponed
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
-
- assertPostponedTransition(fragment3, fragment2, null);
-
- // Pop to mBeginningFragment -- should cancel the fragment2 transition and
- // start the mBeginningFragment transaction postponed
-
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
-
- assertPostponedTransition(fragment2, mBeginningFragment, fragment3);
-
- // start the postponed transition
- mBeginningFragment.startPostponedEnterTransition();
-
- // make sure it ran
- assertBackTransition(fragment2, mBeginningFragment);
- }
-
- // Do a bunch of things to one fragment in a transaction and see if it can screw things up.
- @Test
- public void crazyTransition() throws Throwable {
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final View startBlue = mActivityRule.getActivity().findViewById(R.id.blueSquare);
-
- final PostponedFragment2 fragment2 = new PostponedFragment2();
-
- fm.beginTransaction()
- .addSharedElement(startBlue, "blueSquare")
- .hide(mBeginningFragment)
- .replace(R.id.fragmentContainer, fragment2)
- .hide(fragment2)
- .detach(fragment2)
- .attach(fragment2)
- .show(fragment2)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
-
- FragmentTestUtil.waitForExecution(mActivityRule);
-
- assertPostponedTransition(mBeginningFragment, fragment2, null);
-
- // start the postponed transition
- fragment2.startPostponedEnterTransition();
-
- // make sure it ran
- assertForwardTransition(mBeginningFragment, fragment2);
-
- // Pop back to fragment2, but it should be postponed
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
-
- assertPostponedTransition(fragment2, mBeginningFragment, null);
-
- // start the postponed transition
- mBeginningFragment.startPostponedEnterTransition();
-
- // make sure it ran
- assertBackTransition(fragment2, mBeginningFragment);
- }
-
- // Execute transactions on different containers and ensure that they don't conflict
- @Test
- public void differentContainers() throws Throwable {
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- fm.beginTransaction()
- .remove(mBeginningFragment)
- .setReorderingAllowed(true)
- .commit();
- FragmentTestUtil.waitForExecution(mActivityRule);
- FragmentTestUtil.setContentView(mActivityRule, R.layout.double_container);
-
- TransitionFragment fragment1 = new PostponedFragment1();
- TransitionFragment fragment2 = new PostponedFragment1();
-
- fm.beginTransaction()
- .add(R.id.fragmentContainer1, fragment1)
- .add(R.id.fragmentContainer2, fragment2)
- .setReorderingAllowed(true)
- .commit();
- FragmentTestUtil.waitForExecution(mActivityRule);
- fragment1.startPostponedEnterTransition();
- fragment2.startPostponedEnterTransition();
- fragment1.waitForTransition();
- fragment2.waitForTransition();
- clearTargets(fragment1);
- clearTargets(fragment2);
-
- final View startBlue1 = fragment1.requireView().findViewById(R.id.blueSquare);
- final View startBlue2 = fragment2.requireView().findViewById(R.id.blueSquare);
-
- final TransitionFragment fragment3 = new PostponedFragment2();
-
- fm.beginTransaction()
- .addSharedElement(startBlue1, "blueSquare")
- .replace(R.id.fragmentContainer1, fragment3)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
-
- FragmentTestUtil.waitForExecution(mActivityRule);
-
- assertPostponedTransition(fragment1, fragment3, null);
-
- final TransitionFragment fragment4 = new PostponedFragment2();
-
- fm.beginTransaction()
- .addSharedElement(startBlue2, "blueSquare")
- .replace(R.id.fragmentContainer2, fragment4)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
-
- FragmentTestUtil.waitForExecution(mActivityRule);
-
- assertPostponedTransition(fragment1, fragment3, null);
- assertPostponedTransition(fragment2, fragment4, null);
-
- // start the postponed transition
- fragment3.startPostponedEnterTransition();
-
- // make sure only one ran
- assertForwardTransition(fragment1, fragment3);
- assertPostponedTransition(fragment2, fragment4, null);
-
- // start the postponed transition
- fragment4.startPostponedEnterTransition();
-
- // make sure it ran
- assertForwardTransition(fragment2, fragment4);
-
- // Pop back to fragment2 -- should be postponed
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
-
- assertPostponedTransition(fragment4, fragment2, null);
-
- // Pop back to fragment1 -- also should be postponed
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
-
- assertPostponedTransition(fragment4, fragment2, null);
- assertPostponedTransition(fragment3, fragment1, null);
-
- // start the postponed transition
- fragment2.startPostponedEnterTransition();
-
- // make sure it ran
- assertBackTransition(fragment4, fragment2);
-
- // but not the postponed one
- assertPostponedTransition(fragment3, fragment1, null);
-
- // start the postponed transition
- fragment1.startPostponedEnterTransition();
-
- // make sure it ran
- assertBackTransition(fragment3, fragment1);
- }
-
- // Execute transactions on different containers and ensure that they don't conflict.
- // The postponement can be started out-of-order
- @Test
- public void outOfOrderContainers() throws Throwable {
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- fm.beginTransaction()
- .remove(mBeginningFragment)
- .setReorderingAllowed(true)
- .commit();
- FragmentTestUtil.waitForExecution(mActivityRule);
- FragmentTestUtil.setContentView(mActivityRule, R.layout.double_container);
-
- TransitionFragment fragment1 = new PostponedFragment1();
- TransitionFragment fragment2 = new PostponedFragment1();
-
- fm.beginTransaction()
- .add(R.id.fragmentContainer1, fragment1)
- .add(R.id.fragmentContainer2, fragment2)
- .setReorderingAllowed(true)
- .commit();
- FragmentTestUtil.waitForExecution(mActivityRule);
- fragment1.startPostponedEnterTransition();
- fragment2.startPostponedEnterTransition();
- fragment1.waitForTransition();
- fragment2.waitForTransition();
- clearTargets(fragment1);
- clearTargets(fragment2);
-
- final View startBlue1 = fragment1.requireView().findViewById(R.id.blueSquare);
- final View startBlue2 = fragment2.requireView().findViewById(R.id.blueSquare);
-
- final TransitionFragment fragment3 = new PostponedFragment2();
-
- fm.beginTransaction()
- .addSharedElement(startBlue1, "blueSquare")
- .replace(R.id.fragmentContainer1, fragment3)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
-
- FragmentTestUtil.waitForExecution(mActivityRule);
-
- assertPostponedTransition(fragment1, fragment3, null);
-
- final TransitionFragment fragment4 = new PostponedFragment2();
-
- fm.beginTransaction()
- .addSharedElement(startBlue2, "blueSquare")
- .replace(R.id.fragmentContainer2, fragment4)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
-
- FragmentTestUtil.waitForExecution(mActivityRule);
-
- assertPostponedTransition(fragment1, fragment3, null);
- assertPostponedTransition(fragment2, fragment4, null);
-
- // start the postponed transition
- fragment4.startPostponedEnterTransition();
-
- // make sure only one ran
- assertForwardTransition(fragment2, fragment4);
- assertPostponedTransition(fragment1, fragment3, null);
-
- // start the postponed transition
- fragment3.startPostponedEnterTransition();
-
- // make sure it ran
- assertForwardTransition(fragment1, fragment3);
-
- // Pop back to fragment2 -- should be postponed
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
-
- assertPostponedTransition(fragment4, fragment2, null);
-
- // Pop back to fragment1 -- also should be postponed
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
-
- assertPostponedTransition(fragment4, fragment2, null);
- assertPostponedTransition(fragment3, fragment1, null);
-
- // start the postponed transition
- fragment1.startPostponedEnterTransition();
-
- // make sure it ran
- assertBackTransition(fragment3, fragment1);
-
- // but not the postponed one
- assertPostponedTransition(fragment4, fragment2, null);
-
- // start the postponed transition
- fragment2.startPostponedEnterTransition();
-
- // make sure it ran
- assertBackTransition(fragment4, fragment2);
- }
-
- // Make sure that commitNow for a transaction on a different fragment container doesn't
- // affect the postponed transaction
- @Test
- public void commitNowNoEffect() throws Throwable {
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- fm.beginTransaction()
- .remove(mBeginningFragment)
- .setReorderingAllowed(true)
- .commit();
- FragmentTestUtil.waitForExecution(mActivityRule);
- FragmentTestUtil.setContentView(mActivityRule, R.layout.double_container);
-
- final TransitionFragment fragment1 = new PostponedFragment1();
- final TransitionFragment fragment2 = new PostponedFragment1();
-
- fm.beginTransaction()
- .add(R.id.fragmentContainer1, fragment1)
- .add(R.id.fragmentContainer2, fragment2)
- .setReorderingAllowed(true)
- .commit();
- FragmentTestUtil.waitForExecution(mActivityRule);
- fragment1.startPostponedEnterTransition();
- fragment2.startPostponedEnterTransition();
- fragment1.waitForTransition();
- fragment2.waitForTransition();
- clearTargets(fragment1);
- clearTargets(fragment2);
-
- final View startBlue1 = fragment1.requireView().findViewById(R.id.blueSquare);
- final View startBlue2 = fragment2.requireView().findViewById(R.id.blueSquare);
-
- final TransitionFragment fragment3 = new PostponedFragment2();
- final StrictFragment strictFragment1 = new StrictFragment();
-
- fm.beginTransaction()
- .addSharedElement(startBlue1, "blueSquare")
- .replace(R.id.fragmentContainer1, fragment3)
- .add(strictFragment1, "1")
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
-
- FragmentTestUtil.waitForExecution(mActivityRule);
-
- assertPostponedTransition(fragment1, fragment3, null);
-
- final TransitionFragment fragment4 = new PostponedFragment2();
- final StrictFragment strictFragment2 = new StrictFragment();
-
- mInstrumentation.runOnMainSync(new Runnable() {
- @Override
- public void run() {
- fm.beginTransaction()
- .addSharedElement(startBlue2, "blueSquare")
- .replace(R.id.fragmentContainer2, fragment4)
- .remove(strictFragment1)
- .add(strictFragment2, "2")
- .setReorderingAllowed(true)
- .commitNow();
- }
- });
-
- FragmentTestUtil.waitForExecution(mActivityRule);
-
- assertPostponedTransition(fragment1, fragment3, null);
- assertPostponedTransition(fragment2, fragment4, null);
-
- // start the postponed transition
- fragment4.startPostponedEnterTransition();
-
- // make sure only one ran
- assertForwardTransition(fragment2, fragment4);
- assertPostponedTransition(fragment1, fragment3, null);
-
- // start the postponed transition
- fragment3.startPostponedEnterTransition();
-
- // make sure it ran
- assertForwardTransition(fragment1, fragment3);
- }
-
- // Make sure that commitNow for a transaction affecting a postponed fragment in the same
- // container forces the postponed transition to start.
- @Test
- public void commitNowStartsPostponed() throws Throwable {
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final View startBlue1 = mBeginningFragment.requireView().findViewById(R.id.blueSquare);
-
- final TransitionFragment fragment2 = new PostponedFragment2();
- final TransitionFragment fragment1 = new PostponedFragment1();
-
- fm.beginTransaction()
- .addSharedElement(startBlue1, "blueSquare")
- .replace(R.id.fragmentContainer, fragment2)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- FragmentTestUtil.waitForExecution(mActivityRule);
-
- final View startBlue2 = fragment2.requireView().findViewById(R.id.blueSquare);
-
- mInstrumentation.runOnMainSync(new Runnable() {
- @Override
- public void run() {
- fm.beginTransaction()
- .addSharedElement(startBlue2, "blueSquare")
- .replace(R.id.fragmentContainer, fragment1)
- .setReorderingAllowed(true)
- .commitNow();
- }
- });
-
- assertPostponedTransition(fragment2, fragment1, mBeginningFragment);
-
- // start the postponed transition
- fragment1.startPostponedEnterTransition();
-
- assertForwardTransition(fragment2, fragment1);
- }
-
- // Make sure that when a transaction that removes a view is postponed that
- // another transaction doesn't accidentally remove the view early.
- @Test
- public void noAccidentalRemoval() throws Throwable {
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- fm.beginTransaction()
- .remove(mBeginningFragment)
- .setReorderingAllowed(true)
- .commit();
- FragmentTestUtil.waitForExecution(mActivityRule);
- FragmentTestUtil.setContentView(mActivityRule, R.layout.double_container);
-
- TransitionFragment fragment1 = new PostponedFragment1();
-
- fm.beginTransaction()
- .add(R.id.fragmentContainer1, fragment1)
- .setReorderingAllowed(true)
- .commit();
- FragmentTestUtil.waitForExecution(mActivityRule);
- fragment1.startPostponedEnterTransition();
- fragment1.waitForTransition();
- clearTargets(fragment1);
-
- TransitionFragment fragment2 = new PostponedFragment2();
- // Create a postponed transaction that removes a view
- fm.beginTransaction()
- .replace(R.id.fragmentContainer1, fragment2)
- .setReorderingAllowed(true)
- .commit();
- FragmentTestUtil.waitForExecution(mActivityRule);
- assertPostponedTransition(fragment1, fragment2, null);
-
- TransitionFragment fragment3 = new PostponedFragment1();
- // Create a transaction that doesn't interfere with the previously postponed one
- fm.beginTransaction()
- .replace(R.id.fragmentContainer2, fragment3)
- .setReorderingAllowed(true)
- .commit();
- FragmentTestUtil.waitForExecution(mActivityRule);
-
- assertPostponedTransition(fragment1, fragment2, null);
-
- fragment3.startPostponedEnterTransition();
- fragment3.waitForTransition();
- clearTargets(fragment3);
-
- assertPostponedTransition(fragment1, fragment2, null);
- }
-
- // Ensure that a postponed transaction that is popped runs immediately and that
- // the transaction results in the original state with no transition.
- @Test
- public void popPostponedTransaction() throws Throwable {
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final View startBlue = mBeginningFragment.requireView().findViewById(R.id.blueSquare);
-
- final TransitionFragment fragment = new PostponedFragment2();
-
- fm.beginTransaction()
- .addSharedElement(startBlue, "blueSquare")
- .replace(R.id.fragmentContainer, fragment)
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- FragmentTestUtil.waitForExecution(mActivityRule);
-
- assertPostponedTransition(mBeginningFragment, fragment, null);
-
- FragmentTestUtil.popBackStackImmediate(mActivityRule);
-
- fragment.waitForNoTransition();
- mBeginningFragment.waitForNoTransition();
-
- assureNoTransition(fragment);
- assureNoTransition(mBeginningFragment);
-
- assertFalse(fragment.isAdded());
- assertNull(fragment.getView());
- assertNotNull(mBeginningFragment.getView());
- assertEquals(View.VISIBLE, mBeginningFragment.getView().getVisibility());
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
- assertEquals(1f, mBeginningFragment.getView().getAlpha(), 0f);
- }
- assertTrue(mBeginningFragment.getView().isAttachedToWindow());
- }
-
- // Make sure that when saving the state during a postponed transaction that it saves
- // the state as if it wasn't postponed.
- @Test
- public void saveWhilePostponed() throws Throwable {
- final FragmentController fc1 = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc1, null);
-
- final FragmentManager fm1 = fc1.getSupportFragmentManager();
-
- PostponedFragment1 fragment1 = new PostponedFragment1();
- fm1.beginTransaction()
- .add(R.id.fragmentContainer, fragment1, "1")
- .addToBackStack(null)
- .setReorderingAllowed(true)
- .commit();
- FragmentTestUtil.waitForExecution(mActivityRule);
-
- Pair<Parcelable, FragmentManagerNonConfig> state =
- FragmentTestUtil.destroy(mActivityRule, fc1);
-
- final FragmentController fc2 = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc2, state);
-
- final FragmentManager fm2 = fc2.getSupportFragmentManager();
- Fragment fragment2 = fm2.findFragmentByTag("1");
- assertNotNull(fragment2);
- assertNotNull(fragment2.getView());
- assertEquals(View.VISIBLE, fragment2.getView().getVisibility());
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
- assertEquals(1f, fragment2.getView().getAlpha(), 0f);
- }
- assertTrue(fragment2.isResumed());
- assertTrue(fragment2.isAdded());
- assertTrue(fragment2.getView().isAttachedToWindow());
-
- mInstrumentation.runOnMainSync(new Runnable() {
- @Override
- public void run() {
- assertTrue(fm2.popBackStackImmediate());
-
- }
- });
-
- assertFalse(fragment2.isResumed());
- assertFalse(fragment2.isAdded());
- assertNull(fragment2.getView());
- }
-
- // Ensure that the postponed fragment transactions don't allow reentrancy in fragment manager
- @Test
- public void postponeDoesNotAllowReentrancy() throws Throwable {
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final View startBlue = mActivityRule.getActivity().findViewById(R.id.blueSquare);
-
- final CommitNowFragment fragment = new CommitNowFragment();
- fm.beginTransaction()
- .addSharedElement(startBlue, "blueSquare")
- .replace(R.id.fragmentContainer, fragment)
- .setReorderingAllowed(true)
- .addToBackStack(null)
- .commit();
-
- FragmentTestUtil.waitForExecution(mActivityRule);
-
- // should be postponed now
- assertPostponedTransition(mBeginningFragment, fragment, null);
-
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- // start the postponed transition
- fragment.startPostponedEnterTransition();
-
- try {
- // This should trigger an IllegalStateException
- fm.executePendingTransactions();
- fail("commitNow() while executing a transaction should cause an "
- + "IllegalStateException");
- } catch (IllegalStateException e) {
- // expected
- }
- }
- });
- }
-
- private void assertPostponedTransition(TransitionFragment fromFragment,
- TransitionFragment toFragment, TransitionFragment removedFragment)
- throws InterruptedException {
- if (removedFragment != null) {
- assertNull(removedFragment.getView());
- assureNoTransition(removedFragment);
- }
-
- toFragment.waitForNoTransition();
- assertNotNull(fromFragment.getView());
- assertNotNull(toFragment.getView());
- assertTrue(fromFragment.getView().isAttachedToWindow());
- assertTrue(toFragment.getView().isAttachedToWindow());
- assertEquals(View.VISIBLE, fromFragment.getView().getVisibility());
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
- assertEquals(View.VISIBLE, toFragment.getView().getVisibility());
- assertEquals(0f, toFragment.getView().getAlpha(), 0f);
- } else {
- assertEquals(View.INVISIBLE, toFragment.getView().getVisibility());
- }
- assureNoTransition(fromFragment);
- assureNoTransition(toFragment);
- assertTrue(fromFragment.isResumed());
- assertFalse(toFragment.isResumed());
- }
-
- private void clearTargets(TransitionFragment fragment) {
- fragment.enterTransition.targets.clear();
- fragment.reenterTransition.targets.clear();
- fragment.exitTransition.targets.clear();
- fragment.returnTransition.targets.clear();
- fragment.sharedElementEnter.targets.clear();
- fragment.sharedElementReturn.targets.clear();
- }
-
- private void assureNoTransition(TransitionFragment fragment) {
- assertEquals(0, fragment.enterTransition.targets.size());
- assertEquals(0, fragment.reenterTransition.targets.size());
- assertEquals(0, fragment.enterTransition.targets.size());
- assertEquals(0, fragment.returnTransition.targets.size());
- assertEquals(0, fragment.sharedElementEnter.targets.size());
- assertEquals(0, fragment.sharedElementReturn.targets.size());
- }
-
- private void assertForwardTransition(TransitionFragment start, TransitionFragment end)
- throws InterruptedException {
- start.waitForTransition();
- end.waitForTransition();
- assertEquals(0, start.enterTransition.targets.size());
- assertEquals(1, end.enterTransition.targets.size());
-
- assertEquals(0, start.reenterTransition.targets.size());
- assertEquals(0, end.reenterTransition.targets.size());
-
- assertEquals(0, start.returnTransition.targets.size());
- assertEquals(0, end.returnTransition.targets.size());
-
- assertEquals(1, start.exitTransition.targets.size());
- assertEquals(0, end.exitTransition.targets.size());
-
- assertEquals(0, start.sharedElementEnter.targets.size());
- assertEquals(2, end.sharedElementEnter.targets.size());
-
- assertEquals(0, start.sharedElementReturn.targets.size());
- assertEquals(0, end.sharedElementReturn.targets.size());
-
- final View blue = end.requireView().findViewById(R.id.blueSquare);
- assertTrue(end.sharedElementEnter.targets.contains(blue));
- assertEquals("blueSquare", end.sharedElementEnter.targets.get(0).getTransitionName());
- assertEquals("blueSquare", end.sharedElementEnter.targets.get(1).getTransitionName());
-
- assertNoTargets(start);
- assertNoTargets(end);
-
- clearTargets(start);
- clearTargets(end);
- }
-
- private void assertBackTransition(TransitionFragment start, TransitionFragment end)
- throws InterruptedException {
- start.waitForTransition();
- end.waitForTransition();
- assertEquals(1, end.reenterTransition.targets.size());
- assertEquals(0, start.reenterTransition.targets.size());
-
- assertEquals(0, end.returnTransition.targets.size());
- assertEquals(1, start.returnTransition.targets.size());
-
- assertEquals(0, start.enterTransition.targets.size());
- assertEquals(0, end.enterTransition.targets.size());
-
- assertEquals(0, start.exitTransition.targets.size());
- assertEquals(0, end.exitTransition.targets.size());
-
- assertEquals(0, start.sharedElementEnter.targets.size());
- assertEquals(0, end.sharedElementEnter.targets.size());
-
- assertEquals(2, start.sharedElementReturn.targets.size());
- assertEquals(0, end.sharedElementReturn.targets.size());
-
- final View blue = end.requireView().findViewById(R.id.blueSquare);
- assertTrue(start.sharedElementReturn.targets.contains(blue));
- assertEquals("blueSquare", start.sharedElementReturn.targets.get(0).getTransitionName());
- assertEquals("blueSquare", start.sharedElementReturn.targets.get(1).getTransitionName());
-
- assertNoTargets(end);
- assertNoTargets(start);
-
- clearTargets(start);
- clearTargets(end);
- }
-
- private static void assertNoTargets(TransitionFragment fragment) {
- assertTrue(fragment.enterTransition.getTargets().isEmpty());
- assertTrue(fragment.reenterTransition.getTargets().isEmpty());
- assertTrue(fragment.exitTransition.getTargets().isEmpty());
- assertTrue(fragment.returnTransition.getTargets().isEmpty());
- assertTrue(fragment.sharedElementEnter.getTargets().isEmpty());
- assertTrue(fragment.sharedElementReturn.getTargets().isEmpty());
- }
-
- @ContentView(R.layout.scene1)
- public static class PostponedFragment1 extends TransitionFragment {
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- postponeEnterTransition();
- return super.onCreateView(inflater, container, savedInstanceState);
- }
- }
-
- @ContentView(R.layout.scene2)
- public static class PostponedFragment2 extends TransitionFragment {
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- postponeEnterTransition();
- return super.onCreateView(inflater, container, savedInstanceState);
- }
- }
-
- public static class CommitNowFragment extends PostponedFragment1 {
- @Override
- public void onResume() {
- super.onResume();
- // This should throw because this happens during the execution
- getFragmentManager().beginTransaction()
- .add(R.id.fragmentContainer, new PostponedFragment1())
- .commitNow();
- }
- }
-
- @ContentView(R.layout.scene2)
- public static class TransitionFragment2 extends TransitionFragment {
- }
-}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/PostponedTransitionTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/PostponedTransitionTest.kt
new file mode 100644
index 0000000..5432969
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/PostponedTransitionTest.kt
@@ -0,0 +1,937 @@
+/*
+ * Copyright 2019 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 androidx.fragment.app
+
+import android.os.Build
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.test.FragmentTestActivity
+import androidx.fragment.test.R
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.ActivityTestRule
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.CountDownLatch
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+class PostponedTransitionTest {
+ @get:Rule
+ val activityRule = ActivityTestRule(FragmentTestActivity::class.java)
+
+ private val instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val beginningFragment = PostponedFragment1()
+
+ @Before
+ fun setupContainer() {
+ FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
+ val fm = activityRule.activity.supportFragmentManager
+
+ val backStackLatch = CountDownLatch(1)
+ val backStackListener = object : FragmentManager.OnBackStackChangedListener {
+ override fun onBackStackChanged() {
+ backStackLatch.countDown()
+ fm.removeOnBackStackChangedListener(this)
+ }
+ }
+ fm.addOnBackStackChangedListener(backStackListener)
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, beginningFragment)
+ .setReorderingAllowed(true)
+ .addToBackStack(null)
+ .commit()
+
+ backStackLatch.await()
+
+ beginningFragment.startPostponedEnterTransition()
+ beginningFragment.waitForTransition()
+ clearTargets(beginningFragment)
+ }
+
+ // Ensure that replacing with a fragment that has a postponed transition
+ // will properly postpone it, both adding and popping.
+ @Test
+ fun replaceTransition() {
+ val fm = activityRule.activity.supportFragmentManager
+ val startBlue = activityRule.activity.findViewById<View>(R.id.blueSquare)
+
+ val fragment = PostponedFragment2()
+ fm.beginTransaction()
+ .addSharedElement(startBlue, "blueSquare")
+ .replace(R.id.fragmentContainer, fragment)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+
+ FragmentTestUtil.waitForExecution(activityRule)
+
+ // should be postponed now
+ assertPostponedTransition(beginningFragment, fragment)
+
+ // start the postponed transition
+ fragment.startPostponedEnterTransition()
+
+ // make sure it ran
+ assertForwardTransition(beginningFragment, fragment)
+
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+
+ // should be postponed going back, too
+ assertPostponedTransition(fragment, beginningFragment)
+
+ // start the postponed transition
+ beginningFragment.startPostponedEnterTransition()
+
+ // make sure it ran
+ assertBackTransition(fragment, beginningFragment)
+ }
+
+ // Ensure that replacing a fragment doesn't cause problems with the back stack nesting level
+ @Test
+ fun backStackNestingLevel() {
+ val fm = activityRule.activity.supportFragmentManager
+ var startBlue = activityRule.activity.findViewById<View>(R.id.blueSquare)
+
+ val fragment1 = TransitionFragment(R.layout.scene2)
+ fm.beginTransaction()
+ .addSharedElement(startBlue, "blueSquare")
+ .replace(R.id.fragmentContainer, fragment1)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+
+ // make sure transition ran
+ assertForwardTransition(beginningFragment, fragment1)
+
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+
+ // should be postponed going back
+ assertPostponedTransition(fragment1, beginningFragment)
+
+ // start the postponed transition
+ beginningFragment.startPostponedEnterTransition()
+
+ // make sure it ran
+ assertBackTransition(fragment1, beginningFragment)
+
+ startBlue = activityRule.activity.findViewById(R.id.blueSquare)
+
+ val fragment2 = TransitionFragment(R.layout.scene2)
+ fm.beginTransaction()
+ .addSharedElement(startBlue, "blueSquare")
+ .replace(R.id.fragmentContainer, fragment2)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+
+ // make sure transition ran
+ assertForwardTransition(beginningFragment, fragment2)
+
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+
+ // should be postponed going back
+ assertPostponedTransition(fragment2, beginningFragment)
+
+ // start the postponed transition
+ beginningFragment.startPostponedEnterTransition()
+
+ // make sure it ran
+ assertBackTransition(fragment2, beginningFragment)
+ }
+
+ // Ensure that postponed transition is forced after another has been committed.
+ // This tests when the transactions are executed together
+ @Test
+ fun forcedTransition1() {
+ val fm = activityRule.activity.supportFragmentManager
+ val startBlue = activityRule.activity.findViewById<View>(R.id.blueSquare)
+
+ val fragment2 = PostponedFragment2()
+ val fragment3 = PostponedFragment1()
+
+ var commit = 0
+ // Need to run this on the UI thread so that the transaction doesn't start
+ // between the two
+ instrumentation.runOnMainSync {
+ commit = fm.beginTransaction()
+ .addSharedElement(startBlue, "blueSquare")
+ .replace(R.id.fragmentContainer, fragment2)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+
+ fm.beginTransaction()
+ .addSharedElement(startBlue, "blueSquare")
+ .replace(R.id.fragmentContainer, fragment3)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ }
+ FragmentTestUtil.waitForExecution(activityRule)
+
+ // transition to fragment2 should be started
+ assertForwardTransition(beginningFragment, fragment2)
+
+ // fragment3 should be postponed, but fragment2 should be executed with no transition.
+ assertPostponedTransition(fragment2, fragment3, beginningFragment)
+
+ // start the postponed transition
+ fragment3.startPostponedEnterTransition()
+
+ // make sure it ran
+ assertForwardTransition(fragment2, fragment3)
+
+ FragmentTestUtil.popBackStackImmediate(
+ activityRule, commit,
+ FragmentManager.POP_BACK_STACK_INCLUSIVE
+ )
+
+ assertBackTransition(fragment3, fragment2)
+
+ assertPostponedTransition(fragment2, beginningFragment, fragment3)
+
+ // start the postponed transition
+ beginningFragment.startPostponedEnterTransition()
+
+ // make sure it ran
+ assertBackTransition(fragment2, beginningFragment)
+ }
+
+ // Ensure that postponed transition is forced after another has been committed.
+ // This tests when the transactions are processed separately.
+ @Test
+ fun forcedTransition2() {
+ val fm = activityRule.activity.supportFragmentManager
+ val startBlue = activityRule.activity.findViewById<View>(R.id.blueSquare)
+
+ val fragment2 = PostponedFragment2()
+
+ fm.beginTransaction()
+ .addSharedElement(startBlue, "blueSquare")
+ .replace(R.id.fragmentContainer, fragment2)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+
+ FragmentTestUtil.waitForExecution(activityRule)
+
+ assertPostponedTransition(beginningFragment, fragment2)
+
+ val fragment3 = PostponedFragment1()
+ fm.beginTransaction()
+ .addSharedElement(startBlue, "blueSquare")
+ .replace(R.id.fragmentContainer, fragment3)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+
+ // This should cancel the beginningFragment -> fragment2 transition
+ // and start fragment2 -> fragment3 transition postponed
+ FragmentTestUtil.waitForExecution(activityRule)
+
+ // fragment3 should be postponed, but fragment2 should be executed with no transition.
+ assertPostponedTransition(fragment2, fragment3, beginningFragment)
+
+ // start the postponed transition
+ fragment3.startPostponedEnterTransition()
+
+ // make sure it ran
+ assertForwardTransition(fragment2, fragment3)
+
+ // Pop back to fragment2, but it should be postponed
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+
+ assertPostponedTransition(fragment3, fragment2)
+
+ // Pop to beginningFragment -- should cancel the fragment2 transition and
+ // start the beginningFragment transaction postponed
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+
+ assertPostponedTransition(fragment2, beginningFragment, fragment3)
+
+ // start the postponed transition
+ beginningFragment.startPostponedEnterTransition()
+
+ // make sure it ran
+ assertBackTransition(fragment2, beginningFragment)
+ }
+
+ // Do a bunch of things to one fragment in a transaction and see if it can screw things up.
+ @Test
+ fun crazyTransition() {
+ val fm = activityRule.activity.supportFragmentManager
+ val startBlue = activityRule.activity.findViewById<View>(R.id.blueSquare)
+
+ val fragment2 = PostponedFragment2()
+
+ fm.beginTransaction()
+ .addSharedElement(startBlue, "blueSquare")
+ .hide(beginningFragment)
+ .replace(R.id.fragmentContainer, fragment2)
+ .hide(fragment2)
+ .detach(fragment2)
+ .attach(fragment2)
+ .show(fragment2)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+
+ FragmentTestUtil.waitForExecution(activityRule)
+
+ assertPostponedTransition(beginningFragment, fragment2)
+
+ // start the postponed transition
+ fragment2.startPostponedEnterTransition()
+
+ // make sure it ran
+ assertForwardTransition(beginningFragment, fragment2)
+
+ // Pop back to fragment2, but it should be postponed
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+
+ assertPostponedTransition(fragment2, beginningFragment)
+
+ // start the postponed transition
+ beginningFragment.startPostponedEnterTransition()
+
+ // make sure it ran
+ assertBackTransition(fragment2, beginningFragment)
+ }
+
+ // Execute transactions on different containers and ensure that they don't conflict
+ @Test
+ fun differentContainers() {
+ val fm = activityRule.activity.supportFragmentManager
+ fm.beginTransaction()
+ .remove(beginningFragment)
+ .setReorderingAllowed(true)
+ .commit()
+ FragmentTestUtil.waitForExecution(activityRule)
+ FragmentTestUtil.setContentView(activityRule, R.layout.double_container)
+
+ val fragment1 = PostponedFragment1()
+ val fragment2 = PostponedFragment1()
+
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer1, fragment1)
+ .add(R.id.fragmentContainer2, fragment2)
+ .setReorderingAllowed(true)
+ .commit()
+ FragmentTestUtil.waitForExecution(activityRule)
+ fragment1.startPostponedEnterTransition()
+ fragment2.startPostponedEnterTransition()
+ fragment1.waitForTransition()
+ fragment2.waitForTransition()
+ clearTargets(fragment1)
+ clearTargets(fragment2)
+
+ val startBlue1 = fragment1.requireView().findViewById<View>(R.id.blueSquare)
+ val startBlue2 = fragment2.requireView().findViewById<View>(R.id.blueSquare)
+
+ val fragment3 = PostponedFragment2()
+
+ fm.beginTransaction()
+ .addSharedElement(startBlue1, "blueSquare")
+ .replace(R.id.fragmentContainer1, fragment3)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+
+ FragmentTestUtil.waitForExecution(activityRule)
+
+ assertPostponedTransition(fragment1, fragment3)
+
+ val fragment4 = PostponedFragment2()
+
+ fm.beginTransaction()
+ .addSharedElement(startBlue2, "blueSquare")
+ .replace(R.id.fragmentContainer2, fragment4)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+
+ FragmentTestUtil.waitForExecution(activityRule)
+
+ assertPostponedTransition(fragment1, fragment3)
+ assertPostponedTransition(fragment2, fragment4)
+
+ // start the postponed transition
+ fragment3.startPostponedEnterTransition()
+
+ // make sure only one ran
+ assertForwardTransition(fragment1, fragment3)
+ assertPostponedTransition(fragment2, fragment4)
+
+ // start the postponed transition
+ fragment4.startPostponedEnterTransition()
+
+ // make sure it ran
+ assertForwardTransition(fragment2, fragment4)
+
+ // Pop back to fragment2 -- should be postponed
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+
+ assertPostponedTransition(fragment4, fragment2)
+
+ // Pop back to fragment1 -- also should be postponed
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+
+ assertPostponedTransition(fragment4, fragment2)
+ assertPostponedTransition(fragment3, fragment1)
+
+ // start the postponed transition
+ fragment2.startPostponedEnterTransition()
+
+ // make sure it ran
+ assertBackTransition(fragment4, fragment2)
+
+ // but not the postponed one
+ assertPostponedTransition(fragment3, fragment1)
+
+ // start the postponed transition
+ fragment1.startPostponedEnterTransition()
+
+ // make sure it ran
+ assertBackTransition(fragment3, fragment1)
+ }
+
+ // Execute transactions on different containers and ensure that they don't conflict.
+ // The postponement can be started out-of-order
+ @Test
+ fun outOfOrderContainers() {
+ val fm = activityRule.activity.supportFragmentManager
+ fm.beginTransaction()
+ .remove(beginningFragment)
+ .setReorderingAllowed(true)
+ .commit()
+ FragmentTestUtil.waitForExecution(activityRule)
+ FragmentTestUtil.setContentView(activityRule, R.layout.double_container)
+
+ val fragment1 = PostponedFragment1()
+ val fragment2 = PostponedFragment1()
+
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer1, fragment1)
+ .add(R.id.fragmentContainer2, fragment2)
+ .setReorderingAllowed(true)
+ .commit()
+ FragmentTestUtil.waitForExecution(activityRule)
+ fragment1.startPostponedEnterTransition()
+ fragment2.startPostponedEnterTransition()
+ fragment1.waitForTransition()
+ fragment2.waitForTransition()
+ clearTargets(fragment1)
+ clearTargets(fragment2)
+
+ val startBlue1 = fragment1.requireView().findViewById<View>(R.id.blueSquare)
+ val startBlue2 = fragment2.requireView().findViewById<View>(R.id.blueSquare)
+
+ val fragment3 = PostponedFragment2()
+
+ fm.beginTransaction()
+ .addSharedElement(startBlue1, "blueSquare")
+ .replace(R.id.fragmentContainer1, fragment3)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+
+ FragmentTestUtil.waitForExecution(activityRule)
+
+ assertPostponedTransition(fragment1, fragment3)
+
+ val fragment4 = PostponedFragment2()
+
+ fm.beginTransaction()
+ .addSharedElement(startBlue2, "blueSquare")
+ .replace(R.id.fragmentContainer2, fragment4)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+
+ FragmentTestUtil.waitForExecution(activityRule)
+
+ assertPostponedTransition(fragment1, fragment3)
+ assertPostponedTransition(fragment2, fragment4)
+
+ // start the postponed transition
+ fragment4.startPostponedEnterTransition()
+
+ // make sure only one ran
+ assertForwardTransition(fragment2, fragment4)
+ assertPostponedTransition(fragment1, fragment3)
+
+ // start the postponed transition
+ fragment3.startPostponedEnterTransition()
+
+ // make sure it ran
+ assertForwardTransition(fragment1, fragment3)
+
+ // Pop back to fragment2 -- should be postponed
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+
+ assertPostponedTransition(fragment4, fragment2)
+
+ // Pop back to fragment1 -- also should be postponed
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+
+ assertPostponedTransition(fragment4, fragment2)
+ assertPostponedTransition(fragment3, fragment1)
+
+ // start the postponed transition
+ fragment1.startPostponedEnterTransition()
+
+ // make sure it ran
+ assertBackTransition(fragment3, fragment1)
+
+ // but not the postponed one
+ assertPostponedTransition(fragment4, fragment2)
+
+ // start the postponed transition
+ fragment2.startPostponedEnterTransition()
+
+ // make sure it ran
+ assertBackTransition(fragment4, fragment2)
+ }
+
+ // Make sure that commitNow for a transaction on a different fragment container doesn't
+ // affect the postponed transaction
+ @Test
+ fun commitNowNoEffect() {
+ val fm = activityRule.activity.supportFragmentManager
+ fm.beginTransaction()
+ .remove(beginningFragment)
+ .setReorderingAllowed(true)
+ .commit()
+ FragmentTestUtil.waitForExecution(activityRule)
+ FragmentTestUtil.setContentView(activityRule, R.layout.double_container)
+
+ val fragment1 = PostponedFragment1()
+ val fragment2 = PostponedFragment1()
+
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer1, fragment1)
+ .add(R.id.fragmentContainer2, fragment2)
+ .setReorderingAllowed(true)
+ .commit()
+ FragmentTestUtil.waitForExecution(activityRule)
+ fragment1.startPostponedEnterTransition()
+ fragment2.startPostponedEnterTransition()
+ fragment1.waitForTransition()
+ fragment2.waitForTransition()
+ clearTargets(fragment1)
+ clearTargets(fragment2)
+
+ val startBlue1 = fragment1.requireView().findViewById<View>(R.id.blueSquare)
+ val startBlue2 = fragment2.requireView().findViewById<View>(R.id.blueSquare)
+
+ val fragment3 = PostponedFragment2()
+ val strictFragment1 = StrictFragment()
+
+ fm.beginTransaction()
+ .addSharedElement(startBlue1, "blueSquare")
+ .replace(R.id.fragmentContainer1, fragment3)
+ .add(strictFragment1, "1")
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+
+ FragmentTestUtil.waitForExecution(activityRule)
+
+ assertPostponedTransition(fragment1, fragment3)
+
+ val fragment4 = PostponedFragment2()
+ val strictFragment2 = StrictFragment()
+
+ instrumentation.runOnMainSync {
+ fm.beginTransaction()
+ .addSharedElement(startBlue2, "blueSquare")
+ .replace(R.id.fragmentContainer2, fragment4)
+ .remove(strictFragment1)
+ .add(strictFragment2, "2")
+ .setReorderingAllowed(true)
+ .commitNow()
+ }
+
+ FragmentTestUtil.waitForExecution(activityRule)
+
+ assertPostponedTransition(fragment1, fragment3)
+ assertPostponedTransition(fragment2, fragment4)
+
+ // start the postponed transition
+ fragment4.startPostponedEnterTransition()
+
+ // make sure only one ran
+ assertForwardTransition(fragment2, fragment4)
+ assertPostponedTransition(fragment1, fragment3)
+
+ // start the postponed transition
+ fragment3.startPostponedEnterTransition()
+
+ // make sure it ran
+ assertForwardTransition(fragment1, fragment3)
+ }
+
+ // Make sure that commitNow for a transaction affecting a postponed fragment in the same
+ // container forces the postponed transition to start.
+ @Test
+ fun commitNowStartsPostponed() {
+ val fm = activityRule.activity.supportFragmentManager
+ val startBlue1 = beginningFragment.requireView().findViewById<View>(R.id.blueSquare)
+
+ val fragment2 = PostponedFragment2()
+ val fragment1 = PostponedFragment1()
+
+ fm.beginTransaction()
+ .addSharedElement(startBlue1, "blueSquare")
+ .replace(R.id.fragmentContainer, fragment2)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ FragmentTestUtil.waitForExecution(activityRule)
+
+ val startBlue2 = fragment2.requireView().findViewById<View>(R.id.blueSquare)
+
+ instrumentation.runOnMainSync {
+ fm.beginTransaction()
+ .addSharedElement(startBlue2, "blueSquare")
+ .replace(R.id.fragmentContainer, fragment1)
+ .setReorderingAllowed(true)
+ .commitNow()
+ }
+
+ assertPostponedTransition(fragment2, fragment1, beginningFragment)
+
+ // start the postponed transition
+ fragment1.startPostponedEnterTransition()
+
+ assertForwardTransition(fragment2, fragment1)
+ }
+
+ // Make sure that when a transaction that removes a view is postponed that
+ // another transaction doesn't accidentally remove the view early.
+ @Test
+ fun noAccidentalRemoval() {
+ val fm = activityRule.activity.supportFragmentManager
+ fm.beginTransaction()
+ .remove(beginningFragment)
+ .setReorderingAllowed(true)
+ .commit()
+ FragmentTestUtil.waitForExecution(activityRule)
+ FragmentTestUtil.setContentView(activityRule, R.layout.double_container)
+
+ val fragment1 = PostponedFragment1()
+
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer1, fragment1)
+ .setReorderingAllowed(true)
+ .commit()
+ FragmentTestUtil.waitForExecution(activityRule)
+ fragment1.startPostponedEnterTransition()
+ fragment1.waitForTransition()
+ clearTargets(fragment1)
+
+ val fragment2 = PostponedFragment2()
+ // Create a postponed transaction that removes a view
+ fm.beginTransaction()
+ .replace(R.id.fragmentContainer1, fragment2)
+ .setReorderingAllowed(true)
+ .commit()
+ FragmentTestUtil.waitForExecution(activityRule)
+ assertPostponedTransition(fragment1, fragment2)
+
+ val fragment3 = PostponedFragment1()
+ // Create a transaction that doesn't interfere with the previously postponed one
+ fm.beginTransaction()
+ .replace(R.id.fragmentContainer2, fragment3)
+ .setReorderingAllowed(true)
+ .commit()
+ FragmentTestUtil.waitForExecution(activityRule)
+
+ assertPostponedTransition(fragment1, fragment2)
+
+ fragment3.startPostponedEnterTransition()
+ fragment3.waitForTransition()
+ clearTargets(fragment3)
+
+ assertPostponedTransition(fragment1, fragment2)
+ }
+
+ // Ensure that a postponed transaction that is popped runs immediately and that
+ // the transaction results in the original state with no transition.
+ @Test
+ fun popPostponedTransaction() {
+ val fm = activityRule.activity.supportFragmentManager
+ val startBlue = beginningFragment.requireView().findViewById<View>(R.id.blueSquare)
+
+ val fragment = PostponedFragment2()
+
+ fm.beginTransaction()
+ .addSharedElement(startBlue, "blueSquare")
+ .replace(R.id.fragmentContainer, fragment)
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ FragmentTestUtil.waitForExecution(activityRule)
+
+ assertPostponedTransition(beginningFragment, fragment)
+
+ FragmentTestUtil.popBackStackImmediate(activityRule)
+
+ fragment.waitForNoTransition()
+ beginningFragment.waitForNoTransition()
+
+ assureNoTransition(fragment)
+ assureNoTransition(beginningFragment)
+
+ assertThat(fragment.isAdded).isFalse()
+ assertThat(fragment.view).isNull()
+ assertThat(beginningFragment.view).isNotNull()
+ assertThat(beginningFragment.requireView().visibility).isEqualTo(View.VISIBLE)
+ assertThat(beginningFragment.requireView().alpha).isWithin(0f).of(1f)
+ assertThat(beginningFragment.requireView().isAttachedToWindow).isTrue()
+ }
+
+ // Make sure that when saving the state during a postponed transaction that it saves
+ // the state as if it wasn't postponed.
+ @Test
+ fun saveWhilePostponed() {
+ val fc1 = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc1, null)
+
+ val fm1 = fc1.supportFragmentManager
+
+ val fragment1 = PostponedFragment1()
+ fm1.beginTransaction()
+ .add(R.id.fragmentContainer, fragment1, "1")
+ .addToBackStack(null)
+ .setReorderingAllowed(true)
+ .commit()
+ FragmentTestUtil.waitForExecution(activityRule)
+
+ val state = FragmentTestUtil.destroy(activityRule, fc1)
+
+ val fc2 = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc2, state)
+
+ val fm2 = fc2.supportFragmentManager
+ val fragment2 = fm2.findFragmentByTag("1")!!
+ assertThat(fragment2).isNotNull()
+ assertThat(fragment2.requireView()).isNotNull()
+ assertThat(fragment2.requireView().visibility).isEqualTo(View.VISIBLE)
+ assertThat(fragment2.requireView().alpha).isWithin(0f).of(1f)
+ assertThat(fragment2.isResumed).isTrue()
+ assertThat(fragment2.isAdded).isTrue()
+ assertThat(fragment2.requireView().isAttachedToWindow).isTrue()
+
+ instrumentation.runOnMainSync { assertThat(fm2.popBackStackImmediate()).isTrue() }
+
+ assertThat(fragment2.isResumed).isFalse()
+ assertThat(fragment2.isAdded).isFalse()
+ assertThat(fragment2.view).isNull()
+ }
+
+ // Ensure that the postponed fragment transactions don't allow reentrancy in fragment manager
+ @Test
+ fun postponeDoesNotAllowReentrancy() {
+ val fm = activityRule.activity.supportFragmentManager
+ val startBlue = activityRule.activity.findViewById<View>(R.id.blueSquare)
+
+ val fragment = CommitNowFragment()
+ fm.beginTransaction()
+ .addSharedElement(startBlue, "blueSquare")
+ .replace(R.id.fragmentContainer, fragment)
+ .setReorderingAllowed(true)
+ .addToBackStack(null)
+ .commit()
+
+ FragmentTestUtil.waitForExecution(activityRule)
+
+ // should be postponed now
+ assertPostponedTransition(beginningFragment, fragment)
+
+ activityRule.runOnUiThread {
+ // start the postponed transition
+ fragment.startPostponedEnterTransition()
+
+ try {
+ // This should trigger an IllegalStateException
+ fm.executePendingTransactions()
+ fail("commitNow() while executing a transaction should cause an" +
+ " IllegalStateException")
+ } catch (e: IllegalStateException) {
+ assertThat(e)
+ .hasMessageThat().contains("FragmentManager is already executing transactions")
+ }
+ }
+ }
+
+ private fun assertPostponedTransition(
+ fromFragment: TransitionFragment,
+ toFragment: TransitionFragment,
+ removedFragment: TransitionFragment? = null
+ ) {
+ if (removedFragment != null) {
+ assertThat(removedFragment.view).isNull()
+ assureNoTransition(removedFragment)
+ }
+
+ toFragment.waitForNoTransition()
+ assertThat(fromFragment.view).isNotNull()
+ assertThat(toFragment.view).isNotNull()
+ assertThat(fromFragment.requireView().isAttachedToWindow).isTrue()
+ assertThat(toFragment.requireView().isAttachedToWindow).isTrue()
+ assertThat(fromFragment.requireView().visibility).isEqualTo(View.VISIBLE)
+ assertThat(toFragment.requireView().visibility).isEqualTo(View.VISIBLE)
+ assertThat(toFragment.requireView().alpha).isWithin(0f).of(0f)
+ assureNoTransition(fromFragment)
+ assureNoTransition(toFragment)
+ assertThat(fromFragment.isResumed).isTrue()
+ assertThat(toFragment.isResumed).isFalse()
+ }
+
+ private fun clearTargets(fragment: TransitionFragment) {
+ fragment.enterTransition.targets.clear()
+ fragment.reenterTransition.targets.clear()
+ fragment.exitTransition.targets.clear()
+ fragment.returnTransition.targets.clear()
+ fragment.sharedElementEnter.targets.clear()
+ fragment.sharedElementReturn.targets.clear()
+ }
+
+ private fun assureNoTransition(fragment: TransitionFragment) {
+ assertThat(fragment.enterTransition.targets.size).isEqualTo(0)
+ assertThat(fragment.reenterTransition.targets.size).isEqualTo(0)
+ assertThat(fragment.enterTransition.targets.size).isEqualTo(0)
+ assertThat(fragment.returnTransition.targets.size).isEqualTo(0)
+ assertThat(fragment.sharedElementEnter.targets.size).isEqualTo(0)
+ assertThat(fragment.sharedElementReturn.targets.size).isEqualTo(0)
+ }
+
+ private fun assertForwardTransition(start: TransitionFragment, end: TransitionFragment) {
+ start.waitForTransition()
+ end.waitForTransition()
+ assertThat(start.enterTransition.targets.size).isEqualTo(0)
+ assertThat(end.enterTransition.targets.size).isEqualTo(1)
+
+ assertThat(start.reenterTransition.targets.size).isEqualTo(0)
+ assertThat(end.reenterTransition.targets.size).isEqualTo(0)
+
+ assertThat(start.returnTransition.targets.size).isEqualTo(0)
+ assertThat(end.returnTransition.targets.size).isEqualTo(0)
+
+ assertThat(start.exitTransition.targets.size).isEqualTo(1)
+ assertThat(end.exitTransition.targets.size).isEqualTo(0)
+
+ assertThat(start.sharedElementEnter.targets.size).isEqualTo(0)
+ assertThat(end.sharedElementEnter.targets.size).isEqualTo(2)
+
+ assertThat(start.sharedElementReturn.targets.size).isEqualTo(0)
+ assertThat(end.sharedElementReturn.targets.size).isEqualTo(0)
+
+ val blue = end.requireView().findViewById<View>(R.id.blueSquare)
+ assertThat(end.sharedElementEnter.targets.contains(blue)).isTrue()
+ assertThat(end.sharedElementEnter.targets[0].transitionName).isEqualTo("blueSquare")
+ assertThat(end.sharedElementEnter.targets[1].transitionName).isEqualTo("blueSquare")
+
+ assertNoTargets(start)
+ assertNoTargets(end)
+
+ clearTargets(start)
+ clearTargets(end)
+ }
+
+ private fun assertBackTransition(start: TransitionFragment, end: TransitionFragment) {
+ start.waitForTransition()
+ end.waitForTransition()
+ assertThat(end.reenterTransition.targets.size).isEqualTo(1)
+ assertThat(start.reenterTransition.targets.size).isEqualTo(0)
+
+ assertThat(end.returnTransition.targets.size).isEqualTo(0)
+ assertThat(start.returnTransition.targets.size).isEqualTo(1)
+
+ assertThat(start.enterTransition.targets.size).isEqualTo(0)
+ assertThat(end.enterTransition.targets.size).isEqualTo(0)
+
+ assertThat(start.exitTransition.targets.size).isEqualTo(0)
+ assertThat(end.exitTransition.targets.size).isEqualTo(0)
+
+ assertThat(start.sharedElementEnter.targets.size).isEqualTo(0)
+ assertThat(end.sharedElementEnter.targets.size).isEqualTo(0)
+
+ assertThat(start.sharedElementReturn.targets.size).isEqualTo(2)
+ assertThat(end.sharedElementReturn.targets.size).isEqualTo(0)
+
+ val blue = end.requireView().findViewById<View>(R.id.blueSquare)
+ assertThat(start.sharedElementReturn.targets.contains(blue)).isTrue()
+ assertThat(start.sharedElementReturn.targets[0].transitionName).isEqualTo("blueSquare")
+ assertThat(start.sharedElementReturn.targets[1].transitionName).isEqualTo("blueSquare")
+
+ assertNoTargets(end)
+ assertNoTargets(start)
+
+ clearTargets(start)
+ clearTargets(end)
+ }
+
+ private fun assertNoTargets(fragment: TransitionFragment) {
+ assertThat(fragment.enterTransition.getTargets().isEmpty()).isTrue()
+ assertThat(fragment.reenterTransition.getTargets().isEmpty()).isTrue()
+ assertThat(fragment.exitTransition.getTargets().isEmpty()).isTrue()
+ assertThat(fragment.returnTransition.getTargets().isEmpty()).isTrue()
+ assertThat(fragment.sharedElementEnter.getTargets().isEmpty()).isTrue()
+ assertThat(fragment.sharedElementReturn.getTargets().isEmpty()).isTrue()
+ }
+
+ open class PostponedFragment1 : TransitionFragment(R.layout.scene1) {
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ) = super.onCreateView(inflater, container, savedInstanceState).also {
+ postponeEnterTransition()
+ }
+ }
+
+ class PostponedFragment2 : TransitionFragment(R.layout.scene2) {
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ) = super.onCreateView(inflater, container, savedInstanceState).also {
+ postponeEnterTransition()
+ }
+ }
+
+ class CommitNowFragment : PostponedFragment1() {
+ override fun onResume() {
+ super.onResume()
+ // This should throw because this happens during the execution
+ fragmentManager!!.beginTransaction()
+ .add(R.id.fragmentContainer, PostponedFragment1())
+ .commitNow()
+ }
+ }
+}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/ReentrantFragment.java b/fragment/src/androidTest/java/androidx/fragment/app/ReentrantFragment.java
index ae70d7f..8404459 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/ReentrantFragment.java
+++ b/fragment/src/androidTest/java/androidx/fragment/app/ReentrantFragment.java
@@ -37,7 +37,7 @@
public void onStateChanged(int fromState) {
super.onStateChanged(fromState);
// We execute the transaction when shutting down or after restoring
- if (fromState == mFromState && mState == mToState
+ if (fromState == mFromState && getCurrentState() == mToState
&& (mToState < mFromState || mIsRestored)) {
executeTransaction();
}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/SaveStateFragmentTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/SaveStateFragmentTest.kt
new file mode 100644
index 0000000..304d0bf
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/SaveStateFragmentTest.kt
@@ -0,0 +1,718 @@
+/*
+ * Copyright 2019 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 androidx.fragment.app
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.FragmentTestUtil.HostCallbacks
+import androidx.fragment.app.FragmentTestUtil.createController
+import androidx.fragment.app.FragmentTestUtil.destroy
+import androidx.fragment.app.FragmentTestUtil.restartFragmentController
+import androidx.fragment.app.FragmentTestUtil.resume
+import androidx.fragment.app.FragmentTestUtil.shutdownFragmentController
+import androidx.fragment.app.FragmentTestUtil.startupFragmentController
+import androidx.fragment.app.test.EmptyFragmentTestActivity
+import androidx.fragment.test.R
+import androidx.lifecycle.ViewModelStore
+import androidx.test.annotation.UiThreadTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.rule.ActivityTestRule
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+class SaveStateFragmentTest {
+
+ @get:Rule
+ val activityRule = ActivityTestRule(EmptyFragmentTestActivity::class.java)
+
+ @Test
+ @UiThreadTest
+ fun setInitialSavedState() {
+ val fm = activityRule.activity.supportFragmentManager
+
+ // Add a StateSaveFragment
+ var fragment = StateSaveFragment("Saved", "")
+ fm.beginTransaction().add(fragment, "tag").commit()
+ executePendingTransactions(fm)
+
+ // Change the user visible hint before we save state
+ fragment.userVisibleHint = false
+
+ // Save its state and remove it
+ val state = fm.saveFragmentInstanceState(fragment)
+ fm.beginTransaction().remove(fragment).commit()
+ executePendingTransactions(fm)
+
+ // Create a new instance, calling setInitialSavedState
+ fragment = StateSaveFragment("", "")
+ fragment.setInitialSavedState(state)
+
+ // Add the new instance
+ fm.beginTransaction().add(fragment, "tag").commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("setInitialSavedState did not restore saved state")
+ .that(fragment.savedState).isEqualTo("Saved")
+ assertWithMessage("setInitialSavedState did not restore user visible hint")
+ .that(fragment.userVisibleHint).isEqualTo(false)
+ }
+
+ @Test
+ @UiThreadTest
+ fun setInitialSavedStateWithSetUserVisibleHint() {
+ val fm = activityRule.activity.supportFragmentManager
+
+ // Add a StateSaveFragment
+ var fragment = StateSaveFragment("Saved", "")
+ fm.beginTransaction().add(fragment, "tag").commit()
+ executePendingTransactions(fm)
+
+ // Save its state and remove it
+ val state = fm.saveFragmentInstanceState(fragment)
+ fm.beginTransaction().remove(fragment).commit()
+ executePendingTransactions(fm)
+
+ // Create a new instance, calling setInitialSavedState
+ fragment = StateSaveFragment("", "")
+ fragment.setInitialSavedState(state)
+
+ // Change the user visible hint after we call setInitialSavedState
+ fragment.userVisibleHint = false
+
+ // Add the new instance
+ fm.beginTransaction().add(fragment, "tag").commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("setInitialSavedState did not restore saved state")
+ .that(fragment.savedState).isEqualTo("Saved")
+ assertWithMessage("setUserVisibleHint should override setInitialSavedState")
+ .that(fragment.userVisibleHint).isEqualTo(false)
+ }
+
+ @Test
+ @UiThreadTest
+ fun restoreRetainedInstanceFragments() {
+ // Create a new FragmentManager in isolation, nest some assorted fragments
+ // and then restore them to a second new FragmentManager.
+ val viewModelStore = ViewModelStore()
+ val fc1 = FragmentController.createController(
+ HostCallbacks(activityRule.activity, viewModelStore)
+ )
+
+ val fm1 = fc1.supportFragmentManager
+
+ fc1.attachHost(null)
+ fc1.dispatchCreate()
+
+ // Configure fragments.
+
+ // This retained fragment will be added, then removed. After being removed, it
+ // should no longer be retained by the FragmentManager
+ val removedFragment = StateSaveFragment("Removed", "UnsavedRemoved")
+ removedFragment.retainInstance = true
+ fm1.beginTransaction().add(removedFragment, "tag:removed").commitNow()
+ fm1.beginTransaction().remove(removedFragment).commitNow()
+
+ // This retained fragment will be added, then detached. After being detached, it
+ // should continue to be retained by the FragmentManager
+ val detachedFragment = StateSaveFragment("Detached", "UnsavedDetached")
+ removedFragment.retainInstance = true
+ fm1.beginTransaction().add(detachedFragment, "tag:detached").commitNow()
+ fm1.beginTransaction().detach(detachedFragment).commitNow()
+
+ // Grandparent fragment will not retain instance
+ val grandparentFragment = StateSaveFragment("Grandparent", "UnsavedGrandparent")
+ assertWithMessage("grandparent fragment saved state not initialized")
+ .that(grandparentFragment.savedState).isNotNull()
+ assertWithMessage("grandparent fragment unsaved state not initialized")
+ .that(grandparentFragment.unsavedState).isNotNull()
+ fm1.beginTransaction().add(grandparentFragment, "tag:grandparent").commitNow()
+
+ // Parent fragment will retain instance
+ val parentFragment = StateSaveFragment("Parent", "UnsavedParent")
+ assertWithMessage("parent fragment saved state not initialized")
+ .that(parentFragment.savedState).isNotNull()
+ assertWithMessage("parent fragment unsaved state not initialized")
+ .that(parentFragment.unsavedState).isNotNull()
+ parentFragment.retainInstance = true
+ grandparentFragment.childFragmentManager.beginTransaction()
+ .add(parentFragment, "tag:parent").commitNow()
+ assertWithMessage("parent fragment is not a child of grandparent")
+ .that(parentFragment.parentFragment).isSameAs(grandparentFragment)
+
+ // Child fragment will not retain instance
+ val childFragment = StateSaveFragment("Child", "UnsavedChild")
+ assertWithMessage("child fragment saved state not initialized")
+ .that(childFragment.savedState).isNotNull()
+ assertWithMessage("child fragment unsaved state not initialized")
+ .that(childFragment.unsavedState).isNotNull()
+ parentFragment.childFragmentManager.beginTransaction()
+ .add(childFragment, "tag:child").commitNow()
+ assertWithMessage("child fragment is not a child of grandparent")
+ .that(childFragment.parentFragment).isSameAs(parentFragment)
+
+ // Saved for comparison later
+ val parentChildFragmentManager = parentFragment.childFragmentManager
+
+ fc1.dispatchActivityCreated()
+ fc1.noteStateNotSaved()
+ fc1.execPendingActions()
+ fc1.dispatchStart()
+ fc1.dispatchResume()
+ fc1.execPendingActions()
+
+ // Bring the state back down to destroyed, simulating an activity restart
+ fc1.dispatchPause()
+ val savedState = fc1.saveAllState()
+ fc1.dispatchStop()
+ fc1.dispatchDestroy()
+
+ // Create the new controller and restore state
+ val fc2 = FragmentController.createController(
+ HostCallbacks(activityRule.activity, viewModelStore)
+ )
+
+ val fm2 = fc2.supportFragmentManager
+
+ fc2.attachHost(null)
+ fc2.restoreSaveState(savedState)
+ fc2.dispatchCreate()
+
+ // Confirm that the restored fragments are available and in the expected states
+ val restoredRemovedFragment = fm2.findFragmentByTag("tag:removed") as StateSaveFragment?
+ assertThat(restoredRemovedFragment).isNull()
+ assertWithMessage("Removed Fragment should be destroyed")
+ .that(removedFragment.calledOnDestroy).isTrue()
+
+ val restoredDetachedFragment = fm2.findFragmentByTag("tag:detached") as StateSaveFragment
+ assertThat(restoredDetachedFragment).isNotNull()
+
+ val restoredGrandparent = fm2.findFragmentByTag("tag:grandparent") as StateSaveFragment
+ assertWithMessage("grandparent fragment not restored").that(restoredGrandparent).isNotNull()
+
+ assertWithMessage("grandparent fragment instance was saved")
+ .that(restoredGrandparent).isNotSameAs(grandparentFragment)
+ assertWithMessage("grandparent fragment saved state was not equal")
+ .that(restoredGrandparent.savedState).isEqualTo(grandparentFragment.savedState)
+ assertWithMessage("grandparent fragment unsaved state was unexpectedly preserved")
+ .that(restoredGrandparent.unsavedState).isNotEqualTo(grandparentFragment.unsavedState)
+
+ val restoredParent = restoredGrandparent
+ .childFragmentManager.findFragmentByTag("tag:parent") as StateSaveFragment
+ assertWithMessage("parent fragment not restored").that(restoredParent).isNotNull()
+
+ assertWithMessage("parent fragment instance was not saved")
+ .that(restoredParent).isSameAs(parentFragment)
+ assertWithMessage("parent fragment saved state was not equal")
+ .that(restoredParent.savedState).isEqualTo(parentFragment.savedState)
+ assertWithMessage("parent fragment unsaved state was not equal")
+ .that(restoredParent.unsavedState).isEqualTo(parentFragment.unsavedState)
+ assertWithMessage("parent fragment has the same child FragmentManager")
+ .that(restoredParent.childFragmentManager).isNotSameAs(parentChildFragmentManager)
+
+ val restoredChild = restoredParent
+ .childFragmentManager.findFragmentByTag("tag:child") as StateSaveFragment
+ assertWithMessage("child fragment not restored").that(restoredChild).isNotNull()
+
+ assertWithMessage("child fragment instance state was saved")
+ .that(restoredChild).isNotSameAs(childFragment)
+ assertWithMessage("child fragment saved state was not equal")
+ .that(restoredChild.savedState).isEqualTo(childFragment.savedState)
+ assertWithMessage("child fragment saved state was unexpectedly equal")
+ .that(restoredChild.unsavedState).isNotEqualTo(childFragment.unsavedState)
+
+ fc2.dispatchActivityCreated()
+ fc2.noteStateNotSaved()
+ fc2.execPendingActions()
+ fc2.dispatchStart()
+ fc2.dispatchResume()
+ fc2.execPendingActions()
+
+ // Test that the fragments are in the configuration we expect
+
+ // Bring the state back down to destroyed before we finish the test
+ shutdownFragmentController(fc2, viewModelStore)
+
+ assertWithMessage("grandparent not destroyed")
+ .that(restoredGrandparent.calledOnDestroy).isTrue()
+ assertWithMessage("parent not destroyed").that(restoredParent.calledOnDestroy).isTrue()
+ assertWithMessage("child not destroyed").that(restoredChild.calledOnDestroy).isTrue()
+ }
+
+ @Test
+ @UiThreadTest
+ fun restoreRetainedInstanceFragmentWithTransparentActivityConfigChange() {
+ // Create a new FragmentManager in isolation, add a retained instance Fragment,
+ // then mimic the following scenario:
+ // 1. Activity A adds retained Fragment F
+ // 2. Activity A starts translucent Activity B
+ // 3. Activity B start opaque Activity C
+ // 4. Rotate phone
+ // 5. Finish Activity C
+ // 6. Finish Activity B
+
+ val viewModelStore = ViewModelStore()
+ val fc1 = FragmentController.createController(
+ HostCallbacks(activityRule.activity, viewModelStore)
+ )
+
+ val fm1 = fc1.supportFragmentManager
+
+ fc1.attachHost(null)
+ fc1.dispatchCreate()
+
+ // Add the retained Fragment
+ val retainedFragment = StateSaveFragment("Retained", "UnsavedRetained")
+ retainedFragment.retainInstance = true
+ fm1.beginTransaction().add(retainedFragment, "tag:retained").commitNow()
+
+ // Move the activity to resumed
+ fc1.dispatchActivityCreated()
+ fc1.noteStateNotSaved()
+ fc1.execPendingActions()
+ fc1.dispatchStart()
+ fc1.dispatchResume()
+ fc1.execPendingActions()
+
+ // Launch the transparent activity on top
+ fc1.dispatchPause()
+
+ // Launch the opaque activity on top
+ val savedState = fc1.saveAllState()
+ fc1.dispatchStop()
+
+ // Finish the opaque activity, making our Activity visible i.e., started
+ fc1.noteStateNotSaved()
+ fc1.execPendingActions()
+ fc1.dispatchStart()
+
+ // Finish the transparent activity, causing a config change
+ fc1.dispatchStop()
+ fc1.dispatchDestroy()
+
+ // Create the new controller and restore state
+ val fc2 = FragmentController.createController(
+ HostCallbacks(activityRule.activity, viewModelStore)
+ )
+
+ val fm2 = fc2.supportFragmentManager
+
+ fc2.attachHost(null)
+ fc2.restoreSaveState(savedState)
+ fc2.dispatchCreate()
+
+ val restoredFragment = fm2.findFragmentByTag("tag:retained") as StateSaveFragment
+ assertWithMessage("retained fragment not restored").that(restoredFragment).isNotNull()
+ assertWithMessage("The retained Fragment shouldn't be recreated")
+ .that(restoredFragment).isEqualTo(retainedFragment)
+
+ fc2.dispatchActivityCreated()
+ fc2.noteStateNotSaved()
+ fc2.execPendingActions()
+ fc2.dispatchStart()
+ fc2.dispatchResume()
+ fc2.execPendingActions()
+
+ // Bring the state back down to destroyed before we finish the test
+ shutdownFragmentController(fc2, viewModelStore)
+ }
+
+ @Test
+ @UiThreadTest
+ fun testSavedInstanceStateAfterRestore() {
+
+ val viewModelStore = ViewModelStore()
+ val fc1 =
+ startupFragmentController(activityRule.activity, null, viewModelStore)
+ val fm1 = fc1.supportFragmentManager
+
+ // Add the initial state
+ val parentFragment = StrictFragment()
+ parentFragment.retainInstance = true
+ val childFragment = StrictFragment()
+ fm1.beginTransaction().add(parentFragment, "parent").commitNow()
+ val childFragmentManager = parentFragment.childFragmentManager
+ childFragmentManager.beginTransaction().add(childFragment, "child").commitNow()
+
+ // Confirm the initial state
+ assertWithMessage("Initial parent saved instance state should be null")
+ .that(parentFragment.lastSavedInstanceState).isNull()
+ assertWithMessage("Initial child saved instance state should be null")
+ .that(childFragment.lastSavedInstanceState).isNull()
+
+ // Bring the state back down to destroyed, simulating an activity restart
+ fc1.dispatchPause()
+ val savedState = fc1.saveAllState()
+ fc1.dispatchStop()
+ fc1.dispatchDestroy()
+
+ // Create the new controller and restore state
+ val fc2 = startupFragmentController(
+ activityRule.activity,
+ savedState,
+ viewModelStore
+ )
+ val fm2 = fc2.supportFragmentManager
+
+ val restoredParentFragment = fm2.findFragmentByTag("parent") as StrictFragment
+ assertWithMessage("Parent fragment was not restored")
+ .that(restoredParentFragment).isNotNull()
+ val restoredChildFragment = restoredParentFragment
+ .childFragmentManager.findFragmentByTag("child") as StrictFragment
+ assertWithMessage("Child fragment was not restored").that(restoredChildFragment).isNotNull()
+
+ assertWithMessage("Parent fragment saved instance state should still be null since it is " +
+ "a retained Fragment").that(restoredParentFragment.lastSavedInstanceState).isNull()
+ assertWithMessage("Child fragment saved instance state should be non-null")
+ .that(restoredChildFragment.lastSavedInstanceState).isNotNull()
+
+ // Bring the state back down to destroyed before we finish the test
+ shutdownFragmentController(fc2, viewModelStore)
+ }
+
+ @Test
+ @UiThreadTest
+ fun restoreNestedFragmentsOnBackStack() {
+ val viewModelStore = ViewModelStore()
+ val fc1 = FragmentController.createController(
+ HostCallbacks(activityRule.activity, viewModelStore)
+ )
+
+ val fm1 = fc1.supportFragmentManager
+
+ fc1.attachHost(null)
+ fc1.dispatchCreate()
+
+ // Add the initial state
+ val parentFragment = StrictFragment()
+ val childFragment = StrictFragment()
+ fm1.beginTransaction().add(parentFragment, "parent").commitNow()
+ val childFragmentManager = parentFragment.childFragmentManager
+ childFragmentManager.beginTransaction().add(childFragment, "child").commitNow()
+
+ // Now add a Fragment to the back stack
+ val replacementChildFragment = StrictFragment()
+ childFragmentManager.beginTransaction()
+ .remove(childFragment)
+ .add(replacementChildFragment, "child")
+ .addToBackStack("back_stack").commit()
+ childFragmentManager.executePendingTransactions()
+
+ // Move the activity to resumed
+ fc1.dispatchActivityCreated()
+ fc1.noteStateNotSaved()
+ fc1.execPendingActions()
+ fc1.dispatchStart()
+ fc1.dispatchResume()
+ fc1.execPendingActions()
+
+ // Now bring the state back down
+ fc1.dispatchPause()
+ val savedState = fc1.saveAllState()
+ fc1.dispatchStop()
+ fc1.dispatchDestroy()
+
+ // Create the new controller and restore state
+ val fc2 = FragmentController.createController(
+ HostCallbacks(activityRule.activity, viewModelStore)
+ )
+
+ val fm2 = fc2.supportFragmentManager
+
+ fc2.attachHost(null)
+ fc2.restoreSaveState(savedState)
+ fc2.dispatchCreate()
+
+ val restoredParentFragment = fm2.findFragmentByTag("parent") as StrictFragment
+ assertWithMessage("Parent fragment was not restored")
+ .that(restoredParentFragment).isNotNull()
+ val restoredChildFragment = restoredParentFragment
+ .childFragmentManager.findFragmentByTag("child") as StrictFragment
+ assertWithMessage("Child fragment was not restored").that(restoredChildFragment).isNotNull()
+
+ fc2.dispatchActivityCreated()
+ fc2.noteStateNotSaved()
+ fc2.execPendingActions()
+ fc2.dispatchStart()
+ fc2.dispatchResume()
+ fc2.execPendingActions()
+
+ // Bring the state back down to destroyed before we finish the test
+ shutdownFragmentController(fc2, viewModelStore)
+ }
+
+ /**
+ * When a fragment has been optimized out, it state should still be saved during
+ * save and restore instance state.
+ */
+ @Test
+ @UiThreadTest
+ fun saveRemovedFragment() {
+ var fc = createController(activityRule)
+ resume(activityRule, fc, null)
+ var fm = fc.supportFragmentManager
+
+ var fragment1 = SaveStateFragment.create(1)
+ fm.beginTransaction()
+ .add(android.R.id.content, fragment1, "1")
+ .addToBackStack(null)
+ .commit()
+ var fragment2 = SaveStateFragment.create(2)
+ fm.beginTransaction()
+ .replace(android.R.id.content, fragment2, "2")
+ .addToBackStack(null)
+ .commit()
+ fm.executePendingTransactions()
+
+ val savedState = destroy(activityRule, fc)
+
+ fc = createController(activityRule)
+ resume(activityRule, fc, savedState)
+ fm = fc.supportFragmentManager
+ fragment2 = fm.findFragmentByTag("2") as SaveStateFragment
+ assertThat(fragment2).isNotNull()
+ assertThat(fragment2.value).isEqualTo(2)
+ fm.popBackStackImmediate()
+ fragment1 = fm.findFragmentByTag("1") as SaveStateFragment
+ assertThat(fragment1).isNotNull()
+ assertThat(fragment1.value).isEqualTo(1)
+ }
+
+ /**
+ * Test to ensure that when dispatch* is called that the fragment manager
+ * doesn't cause the contained fragment states to change even if no state changes.
+ */
+ @Test
+ @UiThreadTest
+ fun noPrematureStateChange() {
+ val viewModelStore = ViewModelStore()
+ var fc =
+ startupFragmentController(activityRule.activity, null, viewModelStore)
+ var fm = fc.supportFragmentManager
+
+ fm.beginTransaction().add(StrictFragment(), "1").commitNow()
+
+ fc = restartFragmentController(activityRule.activity, fc, viewModelStore)
+
+ fm = fc.supportFragmentManager
+
+ val fragment1 = fm.findFragmentByTag("1") as StrictFragment
+ assertWithMessage("Fragment should be resumed after restart")
+ .that(fragment1.calledOnResume).isTrue()
+ fragment1.calledOnResume = false
+ fc.dispatchResume()
+
+ assertWithMessage("Fragment should not get onResume() after second dispatchResume()")
+ .that(fragment1.calledOnResume).isFalse()
+ }
+
+ @Test
+ @UiThreadTest
+ fun testIsStateSaved() {
+ val viewModelStore = ViewModelStore()
+ val fc =
+ startupFragmentController(activityRule.activity, null, viewModelStore)
+ val fm = fc.supportFragmentManager
+
+ val f = StrictFragment()
+ fm.beginTransaction().add(f, "1").commitNow()
+
+ assertWithMessage("fragment reported state saved while resumed")
+ .that(f.isStateSaved).isFalse()
+
+ fc.dispatchPause()
+ fc.saveAllState()
+
+ assertWithMessage("fragment reported state not saved after saveAllState")
+ .that(f.isStateSaved).isTrue()
+
+ fc.dispatchStop()
+
+ assertWithMessage("fragment reported state not saved after stop")
+ .that(f.isStateSaved).isTrue()
+
+ viewModelStore.clear()
+ fc.dispatchDestroy()
+
+ assertWithMessage("fragment reported state saved after destroy")
+ .that(f.isStateSaved).isFalse()
+ }
+
+ @Test
+ @UiThreadTest
+ fun saveAnimationState() {
+ val viewModelStore = ViewModelStore()
+ var fc = startupFragmentController(
+ activityRule.activity, null,
+ viewModelStore
+ )
+ var fm = fc.supportFragmentManager
+
+ fm.beginTransaction()
+ .setCustomAnimations(0, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
+ .add(android.R.id.content, SimpleFragment.create(R.layout.fragment_a))
+ .addToBackStack(null)
+ .commit()
+ fm.executePendingTransactions()
+
+ assertAnimationsMatch(fm, 0, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
+
+ // Causes save and restore of fragments and back stack
+ fc = restartFragmentController(activityRule.activity, fc, viewModelStore)
+ fm = fc.supportFragmentManager
+
+ assertAnimationsMatch(fm, 0, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
+
+ fm.beginTransaction()
+ .setCustomAnimations(R.anim.fade_in, R.anim.fade_out, 0, 0)
+ .replace(android.R.id.content, SimpleFragment.create(R.layout.fragment_b))
+ .addToBackStack(null)
+ .commit()
+ fm.executePendingTransactions()
+
+ assertAnimationsMatch(fm, R.anim.fade_in, R.anim.fade_out, 0, 0)
+
+ // Causes save and restore of fragments and back stack
+ fc = restartFragmentController(activityRule.activity, fc, viewModelStore)
+ fm = fc.supportFragmentManager
+
+ assertAnimationsMatch(fm, R.anim.fade_in, R.anim.fade_out, 0, 0)
+
+ fm.popBackStackImmediate()
+
+ assertAnimationsMatch(fm, 0, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
+
+ shutdownFragmentController(fc, viewModelStore)
+ }
+
+ private fun executePendingTransactions(fm: FragmentManager) {
+ activityRule.runOnUiThread { fm.executePendingTransactions() }
+ }
+
+ private fun assertAnimationsMatch(
+ fm: FragmentManager,
+ enter: Int,
+ exit: Int,
+ popEnter: Int,
+ popExit: Int
+ ) {
+ val fmImpl = fm as FragmentManagerImpl
+ val record = fmImpl.mBackStack[fmImpl.mBackStack.size - 1]
+
+ assertThat(record.mEnterAnim).isEqualTo(enter)
+ assertThat(record.mExitAnim).isEqualTo(exit)
+ assertThat(record.mPopEnterAnim).isEqualTo(popEnter)
+ assertThat(record.mPopExitAnim).isEqualTo(popExit)
+ }
+
+ class SimpleFragment : Fragment() {
+ private var layoutId: Int = 0
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (savedInstanceState != null) {
+ layoutId = savedInstanceState.getInt(LAYOUT_ID, layoutId)
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putInt(LAYOUT_ID, layoutId)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View = inflater.inflate(layoutId, container, false)
+
+ companion object {
+ private const val LAYOUT_ID = "layoutId"
+
+ fun create(layoutId: Int): SimpleFragment {
+ val fragment = SimpleFragment()
+ fragment.layoutId = layoutId
+ return fragment
+ }
+ }
+ }
+
+ class SaveStateFragment : Fragment() {
+ var value: Int = 0
+ private set
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putInt(VALUE_KEY, value)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (savedInstanceState != null) {
+ value = savedInstanceState.getInt(VALUE_KEY, value)
+ }
+ }
+
+ companion object {
+ private const val VALUE_KEY = "SaveStateFragment.mValue"
+
+ fun create(value: Int): SaveStateFragment {
+ val saveStateFragment = SaveStateFragment()
+ saveStateFragment.value = value
+ return saveStateFragment
+ }
+ }
+ }
+
+ class StateSaveFragment : StrictFragment {
+
+ var savedState: String? = null
+ private set
+ var unsavedState: String? = null
+
+ constructor()
+
+ constructor(savedState: String, unsavedState: String) {
+ this.savedState = savedState
+ this.unsavedState = unsavedState
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (savedInstanceState != null) {
+ savedState = savedInstanceState.getString(STATE_KEY)
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putString(STATE_KEY, savedState)
+ }
+
+ companion object {
+ private const val STATE_KEY = "state"
+ }
+ }
+}
\ No newline at end of file
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/StrictFragment.java b/fragment/src/androidTest/java/androidx/fragment/app/StrictFragment.java
deleted file mode 100644
index 2acbeda..0000000
--- a/fragment/src/androidTest/java/androidx/fragment/app/StrictFragment.java
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
- * 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 androidx.fragment.app;
-
-import android.content.Context;
-import android.os.Bundle;
-
-/**
- * This fragment watches its primary lifecycle events and throws IllegalStateException
- * if any of them are called out of order or from a bad/unexpected state.
- */
-public class StrictFragment extends Fragment {
- public static final int DETACHED = 0;
- public static final int ATTACHED = 1;
- public static final int CREATED = 2;
- public static final int ACTIVITY_CREATED = 3;
- public static final int STARTED = 4;
- public static final int RESUMED = 5;
-
- int mState;
-
- boolean mCalledOnAttach, mCalledOnCreate, mCalledOnActivityCreated,
- mCalledOnStart, mCalledOnResume, mCalledOnSaveInstanceState,
- mCalledOnPause, mCalledOnStop, mCalledOnDestroy, mCalledOnDetach,
- mCalledOnAttachFragment;
- Bundle mSavedInstanceState;
-
- static String stateToString(int state) {
- switch (state) {
- case DETACHED: return "DETACHED";
- case ATTACHED: return "ATTACHED";
- case CREATED: return "CREATED";
- case ACTIVITY_CREATED: return "ACTIVITY_CREATED";
- case STARTED: return "STARTED";
- case RESUMED: return "RESUMED";
- }
- return "(unknown " + state + ")";
- }
-
- public void onStateChanged(int fromState) {
- checkGetActivity();
- }
-
- public void checkGetActivity() {
- if (getActivity() == null) {
- throw new IllegalStateException("getActivity() returned null at unexpected time");
- }
- }
-
- public void checkState(String caller, int... expected) {
- if (expected == null || expected.length == 0) {
- throw new IllegalArgumentException("must supply at least one expected state");
- }
- for (int expect : expected) {
- if (mState == expect) {
- return;
- }
- }
- final StringBuilder expectString = new StringBuilder(stateToString(expected[0]));
- for (int i = 1; i < expected.length; i++) {
- expectString.append(" or ").append(stateToString(expected[i]));
- }
- throw new IllegalStateException(caller + " called while fragment was "
- + stateToString(mState) + "; expected " + expectString.toString());
- }
-
- public void checkStateAtLeast(String caller, int minState) {
- if (mState < minState) {
- throw new IllegalStateException(caller + " called while fragment was "
- + stateToString(mState) + "; expected at least " + stateToString(minState));
- }
- }
-
- @Override
- public void onAttachFragment(Fragment childFragment) {
- mCalledOnAttachFragment = true;
- }
-
- @Override
- public void onAttach(Context context) {
- super.onAttach(context);
- mCalledOnAttach = true;
- checkState("onAttach", DETACHED);
- mState = ATTACHED;
- onStateChanged(DETACHED);
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (mCalledOnCreate && !mCalledOnDestroy) {
- throw new IllegalStateException("onCreate called more than once with no onDestroy");
- }
- mCalledOnCreate = true;
- mSavedInstanceState = savedInstanceState;
- checkState("onCreate", ATTACHED);
- mState = CREATED;
- onStateChanged(ATTACHED);
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- mCalledOnActivityCreated = true;
- checkState("onActivityCreated", ATTACHED, CREATED);
- int fromState = mState;
- mState = ACTIVITY_CREATED;
- onStateChanged(fromState);
- }
-
- @Override
- public void onStart() {
- super.onStart();
- mCalledOnStart = true;
- checkState("onStart", CREATED, ACTIVITY_CREATED);
- mState = STARTED;
- onStateChanged(ACTIVITY_CREATED);
- }
-
- @Override
- public void onResume() {
- super.onResume();
- mCalledOnResume = true;
- checkState("onResume", STARTED);
- mState = RESUMED;
- onStateChanged(STARTED);
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- mCalledOnSaveInstanceState = true;
- checkGetActivity();
- // FIXME: We should not allow onSaveInstanceState except when STARTED or greater.
- // But FragmentManager currently does it in saveAllState for fragments on the
- // back stack, so fragments may be in the CREATED state.
- checkStateAtLeast("onSaveInstanceState", CREATED);
- }
-
- @Override
- public void onPause() {
- super.onPause();
- mCalledOnPause = true;
- checkState("onPause", RESUMED);
- mState = STARTED;
- onStateChanged(RESUMED);
- }
-
- @Override
- public void onStop() {
- super.onStop();
- mCalledOnStop = true;
- checkState("onStop", STARTED);
- mState = CREATED;
- onStateChanged(STARTED);
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- mCalledOnDestroy = true;
- checkState("onDestroy", CREATED);
- mState = ATTACHED;
- onStateChanged(CREATED);
- }
-
- @Override
- public void onDetach() {
- super.onDetach();
- mCalledOnDetach = true;
- checkState("onDestroy", CREATED, ATTACHED);
- int fromState = mState;
- mState = DETACHED;
- onStateChanged(fromState);
- }
-}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/StrictFragment.kt b/fragment/src/androidTest/java/androidx/fragment/app/StrictFragment.kt
new file mode 100644
index 0000000..b8faca2
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/StrictFragment.kt
@@ -0,0 +1,193 @@
+/*
+ * 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 androidx.fragment.app
+
+import android.content.Context
+import android.os.Bundle
+import com.google.common.truth.Truth.assertWithMessage
+
+/**
+ * This fragment watches its primary lifecycle events and throws IllegalStateException
+ * if any of them are called out of order or from a bad/unexpected state.
+ */
+open class StrictFragment : Fragment() {
+ var currentState: Int = 0
+
+ var calledOnAttach: Boolean = false
+ var calledOnCreate: Boolean = false
+ var calledOnActivityCreated: Boolean = false
+ var calledOnStart: Boolean = false
+ var calledOnResume: Boolean = false
+ var calledOnSaveInstanceState: Boolean = false
+ var calledOnPause: Boolean = false
+ var calledOnStop: Boolean = false
+ var calledOnDestroy: Boolean = false
+ var calledOnDetach: Boolean = false
+ var calledOnAttachFragment: Boolean = false
+ var lastSavedInstanceState: Bundle? = null
+
+ open fun onStateChanged(fromState: Int) {
+ checkGetActivity()
+ }
+
+ fun checkGetActivity() {
+ assertWithMessage("getActivity() returned null at unexpected time")
+ .that(activity)
+ .isNotNull()
+ }
+
+ fun checkState(caller: String, vararg expected: Int) {
+ if (expected.isEmpty()) {
+ throw IllegalArgumentException("must supply at least one expected state")
+ }
+ for (expect in expected) {
+ if (currentState == expect) {
+ return
+ }
+ }
+ val expectString = StringBuilder(stateToString(expected[0]))
+ for (i in 1 until expected.size) {
+ expectString.append(" or ").append(stateToString(expected[i]))
+ }
+ throw IllegalStateException(
+ "$caller called while fragment was ${stateToString(currentState)}; " +
+ "expected $expectString"
+ )
+ }
+
+ fun checkStateAtLeast(caller: String, minState: Int) {
+ if (currentState < minState) {
+ throw IllegalStateException(
+ "$caller called while fragment was ${stateToString(currentState)}; " +
+ "expected at least ${stateToString(minState)}"
+ )
+ }
+ }
+
+ override fun onAttachFragment(childFragment: Fragment) {
+ calledOnAttachFragment = true
+ }
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ calledOnAttach = true
+ checkState("onAttach", DETACHED)
+ currentState = ATTACHED
+ onStateChanged(DETACHED)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (calledOnCreate && !calledOnDestroy) {
+ throw IllegalStateException("onCreate called more than once with no onDestroy")
+ }
+ calledOnCreate = true
+ lastSavedInstanceState = savedInstanceState
+ checkState("onCreate", ATTACHED)
+ currentState = CREATED
+ onStateChanged(ATTACHED)
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+ calledOnActivityCreated = true
+ checkState("onActivityCreated", ATTACHED, CREATED)
+ val fromState = currentState
+ currentState = ACTIVITY_CREATED
+ onStateChanged(fromState)
+ }
+
+ override fun onStart() {
+ super.onStart()
+ calledOnStart = true
+ checkState("onStart", CREATED, ACTIVITY_CREATED)
+ currentState = STARTED
+ onStateChanged(ACTIVITY_CREATED)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ calledOnResume = true
+ checkState("onResume", STARTED)
+ currentState = RESUMED
+ onStateChanged(STARTED)
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ calledOnSaveInstanceState = true
+ checkGetActivity()
+ // FIXME: We should not allow onSaveInstanceState except when STARTED or greater.
+ // But FragmentManager currently does it in saveAllState for fragments on the
+ // back stack, so fragments may be in the CREATED state.
+ checkStateAtLeast("onSaveInstanceState", CREATED)
+ }
+
+ override fun onPause() {
+ super.onPause()
+ calledOnPause = true
+ checkState("onPause", RESUMED)
+ currentState = STARTED
+ onStateChanged(RESUMED)
+ }
+
+ override fun onStop() {
+ super.onStop()
+ calledOnStop = true
+ checkState("onStop", STARTED)
+ currentState = CREATED
+ onStateChanged(STARTED)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ calledOnDestroy = true
+ checkState("onDestroy", CREATED)
+ currentState = ATTACHED
+ onStateChanged(CREATED)
+ }
+
+ override fun onDetach() {
+ super.onDetach()
+ calledOnDetach = true
+ checkState("onDestroy", CREATED, ATTACHED)
+ val fromState = currentState
+ currentState = DETACHED
+ onStateChanged(fromState)
+ }
+
+ companion object {
+ const val DETACHED = 0
+ const val ATTACHED = 1
+ const val CREATED = 2
+ const val ACTIVITY_CREATED = 3
+ const val STARTED = 4
+ const val RESUMED = 5
+
+ internal fun stateToString(state: Int): String {
+ when (state) {
+ DETACHED -> return "DETACHED"
+ ATTACHED -> return "ATTACHED"
+ CREATED -> return "CREATED"
+ ACTIVITY_CREATED -> return "ACTIVITY_CREATED"
+ STARTED -> return "STARTED"
+ RESUMED -> return "RESUMED"
+ }
+ return "(unknown $state)"
+ }
+ }
+}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/StrictViewFragment.java b/fragment/src/androidTest/java/androidx/fragment/app/StrictViewFragment.java
deleted file mode 100644
index ec1a553..0000000
--- a/fragment/src/androidTest/java/androidx/fragment/app/StrictViewFragment.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * 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 androidx.fragment.app;
-
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.test.R;
-
-public class StrictViewFragment extends StrictFragment {
- boolean mOnCreateViewCalled, mOnViewCreatedCalled, mOnDestroyViewCalled;
- int mLayoutId = R.layout.strict_view_fragment;
-
- public void setLayoutId(int layoutId) {
- mLayoutId = layoutId;
- }
-
- public static StrictViewFragment create(int layoutId) {
- StrictViewFragment fragment = new StrictViewFragment();
- fragment.mLayoutId = layoutId;
- return fragment;
- }
-
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- checkGetActivity();
- checkState("onCreateView", CREATED);
- View result = super.onCreateView(inflater, container, savedInstanceState);
- if (result == null) {
- result = inflater.inflate(mLayoutId, container, false);
- }
- mOnCreateViewCalled = true;
- return result;
- }
-
- @Override
- public void onViewCreated(View view, Bundle savedInstanceState) {
- if (view == null) {
- throw new IllegalArgumentException("onViewCreated view argument should not be null");
- }
- checkGetActivity();
- checkState("onViewCreated", CREATED);
- mOnViewCreatedCalled = true;
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- if (getView() == null) {
- throw new IllegalStateException("getView returned null in onDestroyView");
- }
- checkGetActivity();
- checkState("onDestroyView", CREATED);
- mOnDestroyViewCalled = true;
- }
-}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/StrictViewFragment.kt b/fragment/src/androidTest/java/androidx/fragment/app/StrictViewFragment.kt
new file mode 100644
index 0000000..5b367e4
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/StrictViewFragment.kt
@@ -0,0 +1,65 @@
+/*
+ * 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 androidx.fragment.app
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.LayoutRes
+import androidx.fragment.test.R
+import com.google.common.truth.Truth.assertWithMessage
+
+open class StrictViewFragment(
+ @LayoutRes val contentLayoutId: Int = R.layout.strict_view_fragment
+) : StrictFragment() {
+
+ internal var onCreateViewCalled: Boolean = false
+ internal var onViewCreatedCalled: Boolean = false
+ internal var onDestroyViewCalled: Boolean = false
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ checkGetActivity()
+ checkState("onCreateView", StrictFragment.CREATED)
+ var result = super.onCreateView(inflater, container, savedInstanceState)
+ if (result == null) {
+ result = inflater.inflate(contentLayoutId, container, false)
+ }
+ onCreateViewCalled = true
+ return result
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ checkGetActivity()
+ checkState("onViewCreated", StrictFragment.CREATED)
+ onViewCreatedCalled = true
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ assertWithMessage("getView returned null in onDestroyView")
+ .that(view)
+ .isNotNull()
+ checkGetActivity()
+ checkState("onDestroyView", StrictFragment.CREATED)
+ onDestroyViewCalled = true
+ }
+}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/TargetFragmentLifeCycleTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/TargetFragmentLifeCycleTest.kt
new file mode 100644
index 0000000..0a62f5e
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/TargetFragmentLifeCycleTest.kt
@@ -0,0 +1,427 @@
+/*
+ * Copyright 2019 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 androidx.fragment.app
+
+import android.os.Bundle
+import androidx.fragment.app.test.EmptyFragmentTestActivity
+import androidx.lifecycle.ViewModelStore
+import androidx.test.annotation.UiThreadTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.rule.ActivityTestRule
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+class TargetFragmentLifeCycleTest {
+
+ @get:Rule
+ val activityRule = ActivityTestRule(EmptyFragmentTestActivity::class.java)
+
+ @Test
+ fun targetFragmentNoCycles() {
+ val one = Fragment()
+ val two = Fragment()
+ val three = Fragment()
+
+ try {
+ one.setTargetFragment(two, 0)
+ two.setTargetFragment(three, 0)
+ three.setTargetFragment(one, 0)
+ Assert.fail("creating a fragment target cycle did not throw IllegalArgumentException")
+ } catch (e: IllegalArgumentException) {
+ assertThat(e).hasMessageThat().contains("Setting $one as the target of $three would" +
+ " create a target cycle")
+ }
+ }
+
+ @Test
+ fun targetFragmentSetClear() {
+ val one = Fragment()
+ val two = Fragment()
+
+ one.setTargetFragment(two, 0)
+ one.setTargetFragment(null, 0)
+ }
+
+ /**
+ * Test that target fragments are in a useful state when we restore them, even if they're
+ * on the back stack.
+ */
+ @Test
+ @UiThreadTest
+ fun targetFragmentRestoreLifecycleStateBackStack() {
+ val viewModelStore = ViewModelStore()
+ val fc1 = FragmentController.createController(
+ FragmentTestUtil.HostCallbacks(activityRule.activity, viewModelStore)
+ )
+
+ val fm1 = fc1.supportFragmentManager
+
+ fc1.attachHost(null)
+ fc1.dispatchCreate()
+
+ val target = TargetFragment()
+ fm1.beginTransaction().add(target, "target").commitNow()
+
+ val referrer = ReferrerFragment()
+ referrer.setTargetFragment(target, 0)
+
+ fm1.beginTransaction()
+ .remove(target)
+ .add(referrer, "referrer")
+ .addToBackStack(null)
+ .commit()
+
+ fc1.dispatchActivityCreated()
+ fc1.noteStateNotSaved()
+ fc1.execPendingActions()
+ fc1.dispatchStart()
+ fc1.dispatchResume()
+ fc1.execPendingActions()
+
+ // Simulate an activity restart
+ val fc2 =
+ FragmentTestUtil.restartFragmentController(activityRule.activity, fc1, viewModelStore)
+
+ // Bring the state back down to destroyed before we finish the test
+ FragmentTestUtil.shutdownFragmentController(fc2, viewModelStore)
+ }
+
+ @Test
+ @UiThreadTest
+ fun targetFragmentRestoreLifecycleStateManagerOrder() {
+ val viewModelStore = ViewModelStore()
+ val fc1 = FragmentController.createController(
+ FragmentTestUtil.HostCallbacks(activityRule.activity, viewModelStore)
+ )
+
+ val fm1 = fc1.supportFragmentManager
+
+ fc1.attachHost(null)
+ fc1.dispatchCreate()
+
+ val target1 = TargetFragment()
+ val referrer1 = ReferrerFragment()
+ referrer1.setTargetFragment(target1, 0)
+
+ fm1.beginTransaction().add(target1, "target1").add(referrer1, "referrer1").commitNow()
+
+ val target2 = TargetFragment()
+ val referrer2 = ReferrerFragment()
+ referrer2.setTargetFragment(target2, 0)
+
+ // Order shouldn't matter.
+ fm1.beginTransaction().add(referrer2, "referrer2").add(target2, "target2").commitNow()
+
+ fc1.dispatchActivityCreated()
+ fc1.noteStateNotSaved()
+ fc1.execPendingActions()
+ fc1.dispatchStart()
+ fc1.dispatchResume()
+ fc1.execPendingActions()
+
+ // Simulate an activity restart
+ val fc2 =
+ FragmentTestUtil.restartFragmentController(activityRule.activity, fc1, viewModelStore)
+
+ // Bring the state back down to destroyed before we finish the test
+ FragmentTestUtil.shutdownFragmentController(fc2, viewModelStore)
+ }
+
+ @Test
+ @UiThreadTest
+ fun targetFragmentClearedWhenSetToNull() {
+ val viewModelStore = ViewModelStore()
+ val fc =
+ FragmentTestUtil.startupFragmentController(activityRule.activity, null, viewModelStore)
+
+ val fm = fc.supportFragmentManager
+
+ val target = TargetFragment()
+ val referrer = ReferrerFragment()
+ referrer.setTargetFragment(target, 0)
+
+ assertWithMessage("Target Fragment should be accessible before being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ fm.beginTransaction().add(target, "target").add(referrer, "referrer").commitNow()
+
+ assertWithMessage("Target Fragment should be accessible after being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ referrer.setTargetFragment(null, 0)
+
+ assertWithMessage("Target Fragment should cleared after setTargetFragment with null")
+ .that(referrer.targetFragment).isNull()
+
+ fm.beginTransaction().remove(referrer).commitNow()
+
+ assertWithMessage("Target Fragment should still be cleared after being removed")
+ .that(referrer.targetFragment).isNull()
+
+ FragmentTestUtil.shutdownFragmentController(fc, viewModelStore)
+ }
+
+ @Test
+ @UiThreadTest
+ fun targetFragment_replacement() {
+ val viewModelStore = ViewModelStore()
+ val fc =
+ FragmentTestUtil.startupFragmentController(activityRule.activity, null, viewModelStore)
+
+ val fm = fc.supportFragmentManager
+
+ val referrer = ReferrerFragment()
+ val target = TargetFragment()
+ referrer.setTargetFragment(target, 0)
+
+ assertWithMessage("Target Fragment should be accessible before being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ fm.beginTransaction().add(referrer, "referrer").add(target, "target").commitNow()
+
+ assertWithMessage("Target Fragment should be accessible after being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ val newTarget = TargetFragment()
+ referrer.setTargetFragment(newTarget, 0)
+
+ assertWithMessage("New Target Fragment should returned despite not being added")
+ .that(referrer.targetFragment).isSameAs(newTarget)
+
+ referrer.setTargetFragment(target, 0)
+
+ assertWithMessage("Replacement Target Fragment should override previous target")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ FragmentTestUtil.shutdownFragmentController(fc, viewModelStore)
+ }
+
+ /**
+ * Test the availability of getTargetFragment() when the target Fragment is already
+ * attached to a FragmentManager, but the referrer Fragment is not attached.
+ */
+ @Test
+ @UiThreadTest
+ fun targetFragmentOnlyTargetAdded() {
+ val viewModelStore = ViewModelStore()
+ val fc =
+ FragmentTestUtil.startupFragmentController(activityRule.activity, null, viewModelStore)
+
+ val fm = fc.supportFragmentManager
+
+ val target = TargetFragment()
+ // Add just the target Fragment to the FragmentManager
+ fm.beginTransaction().add(target, "target").commitNow()
+
+ val referrer = ReferrerFragment()
+ referrer.setTargetFragment(target, 0)
+
+ assertWithMessage("Target Fragment should be accessible before being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ fm.beginTransaction().add(referrer, "referrer").commitNow()
+
+ assertWithMessage("Target Fragment should be accessible after being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ fm.beginTransaction().remove(referrer).commitNow()
+
+ assertWithMessage("Target Fragment should be accessible after being removed")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ FragmentTestUtil.shutdownFragmentController(fc, viewModelStore)
+ }
+
+ /**
+ * Test the availability of getTargetFragment() when the target fragment is
+ * not retained and the referrer fragment is not retained.
+ */
+ @Test
+ @UiThreadTest
+ fun targetFragmentNonRetainedNonRetained() {
+ val viewModelStore = ViewModelStore()
+ val fc =
+ FragmentTestUtil.startupFragmentController(activityRule.activity, null, viewModelStore)
+
+ val fm = fc.supportFragmentManager
+
+ val target = TargetFragment()
+ val referrer = ReferrerFragment()
+ referrer.setTargetFragment(target, 0)
+
+ assertWithMessage("Target Fragment should be accessible before being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ fm.beginTransaction().add(target, "target").add(referrer, "referrer").commitNow()
+
+ assertWithMessage("Target Fragment should be accessible after being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ fm.beginTransaction().remove(referrer).commitNow()
+
+ assertWithMessage("Target Fragment should be accessible after being removed")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ FragmentTestUtil.shutdownFragmentController(fc, viewModelStore)
+
+ assertWithMessage("Target Fragment should be accessible after destruction")
+ .that(referrer.targetFragment).isSameAs(target)
+ }
+
+ /**
+ * Test the availability of getTargetFragment() when the target fragment is
+ * retained and the referrer fragment is not retained.
+ */
+ @Test
+ @UiThreadTest
+ fun targetFragmentRetainedNonRetained() {
+ val viewModelStore = ViewModelStore()
+ val fc =
+ FragmentTestUtil.startupFragmentController(activityRule.activity, null, viewModelStore)
+
+ val fm = fc.supportFragmentManager
+
+ val target = TargetFragment()
+ target.retainInstance = true
+ val referrer = ReferrerFragment()
+ referrer.setTargetFragment(target, 0)
+
+ assertWithMessage("Target Fragment should be accessible before being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ fm.beginTransaction().add(target, "target").add(referrer, "referrer").commitNow()
+
+ assertWithMessage("Target Fragment should be accessible after being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ fm.beginTransaction().remove(referrer).commitNow()
+
+ assertWithMessage("Target Fragment should be accessible after being removed")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ FragmentTestUtil.shutdownFragmentController(fc, viewModelStore)
+
+ assertWithMessage("Target Fragment should be accessible after destruction")
+ .that(referrer.targetFragment).isSameAs(target)
+ }
+
+ /**
+ * Test the availability of getTargetFragment() when the target fragment is
+ * not retained and the referrer fragment is retained.
+ */
+ @Test
+ @UiThreadTest
+ fun targetFragmentNonRetainedRetained() {
+ val viewModelStore = ViewModelStore()
+ val fc =
+ FragmentTestUtil.startupFragmentController(activityRule.activity, null, viewModelStore)
+
+ val fm = fc.supportFragmentManager
+
+ val target = TargetFragment()
+ val referrer = ReferrerFragment()
+ referrer.setTargetFragment(target, 0)
+ referrer.retainInstance = true
+
+ assertWithMessage("Target Fragment should be accessible before being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ fm.beginTransaction().add(target, "target").add(referrer, "referrer").commitNow()
+
+ assertWithMessage("Target Fragment should be accessible after being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ // Save the state
+ fc.dispatchPause()
+ fc.saveAllState()
+ fc.dispatchStop()
+ fc.dispatchDestroy()
+
+ assertWithMessage("Target Fragment should be accessible after target Fragment destruction")
+ .that(referrer.targetFragment).isSameAs(target)
+ }
+
+ /**
+ * Test the availability of getTargetFragment() when the target fragment is
+ * retained and the referrer fragment is also retained.
+ */
+ @Test
+ @UiThreadTest
+ fun targetFragmentRetainedRetained() {
+ val viewModelStore = ViewModelStore()
+ val fc =
+ FragmentTestUtil.startupFragmentController(activityRule.activity, null, viewModelStore)
+
+ val fm = fc.supportFragmentManager
+
+ val target = TargetFragment()
+ target.retainInstance = true
+ val referrer = ReferrerFragment()
+ referrer.retainInstance = true
+ referrer.setTargetFragment(target, 0)
+
+ assertWithMessage("Target Fragment should be accessible before being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ fm.beginTransaction().add(target, "target").add(referrer, "referrer").commitNow()
+
+ assertWithMessage("Target Fragment should be accessible after being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ // Save the state
+ fc.dispatchPause()
+ fc.saveAllState()
+ fc.dispatchStop()
+ fc.dispatchDestroy()
+
+ assertWithMessage("Target Fragment should be accessible after FragmentManager destruction")
+ .that(referrer.targetFragment).isSameAs(target)
+ }
+
+ class TargetFragment : Fragment() {
+ var calledCreate: Boolean = false
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ calledCreate = true
+ }
+ }
+
+ class ReferrerFragment : Fragment() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val target = targetFragment
+ assertWithMessage("target fragment was null during referrer onCreate")
+ .that(target).isNotNull()
+
+ if (target !is TargetFragment) {
+ throw IllegalStateException("target fragment was not a TargetFragment")
+ }
+
+ assertWithMessage("target fragment has not yet been created")
+ .that(target.calledCreate).isTrue()
+ }
+ }
+}
\ No newline at end of file
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/TrackingTransition.java b/fragment/src/androidTest/java/androidx/fragment/app/TrackingTransition.java
deleted file mode 100644
index 7b2a494..0000000
--- a/fragment/src/androidTest/java/androidx/fragment/app/TrackingTransition.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * 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 androidx.fragment.app;
-
-import android.animation.Animator;
-import android.graphics.Rect;
-import android.transition.Transition;
-import android.transition.TransitionValues;
-import android.view.View;
-import android.view.ViewGroup;
-
-import java.util.ArrayList;
-
-/**
- * A transition that tracks which targets are applied to it.
- * It will assume any target that it applies to will have differences
- * between the start and end state, regardless of the differences
- * that actually exist. In other words, it doesn't actually check
- * any size or position differences or any other property of the view.
- * It just records the difference.
- * <p>
- * Both start and end value Views are recorded, but no actual animation
- * is created.
- */
-class TrackingTransition extends Transition implements TargetTracking {
- public final ArrayList<View> targets = new ArrayList<>();
- private final Rect[] mEpicenter = new Rect[1];
- private static final String PROP = "tracking:prop";
- private static final String[] PROPS = { PROP };
-
- @Override
- public String[] getTransitionProperties() {
- return PROPS;
- }
-
- @Override
- public void captureStartValues(TransitionValues transitionValues) {
- transitionValues.values.put(PROP, 0);
- }
-
- @Override
- public void captureEndValues(TransitionValues transitionValues) {
- transitionValues.values.put(PROP, 1);
- }
-
- @Override
- public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues,
- TransitionValues endValues) {
- if (startValues != null) {
- targets.add(startValues.view);
- }
- if (endValues != null) {
- targets.add(endValues.view);
- }
- Rect epicenter = getEpicenter();
- if (epicenter != null) {
- mEpicenter[0] = new Rect(epicenter);
- } else {
- mEpicenter[0] = null;
- }
- return null;
- }
-
- @Override
- public ArrayList<View> getTrackedTargets() {
- return targets;
- }
-
- @Override
- public void clearTargets() {
- targets.clear();
- }
-
- @Override
- public Rect getCapturedEpicenter() {
- return mEpicenter[0];
- }
-}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/TrackingTransition.kt b/fragment/src/androidTest/java/androidx/fragment/app/TrackingTransition.kt
new file mode 100644
index 0000000..e45a428
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/TrackingTransition.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2019 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 androidx.fragment.app
+
+import android.graphics.Rect
+import android.transition.Transition
+import android.transition.TransitionValues
+import android.view.View
+import android.view.ViewGroup
+
+import java.util.ArrayList
+
+/**
+ * A transition that tracks which targets are applied to it.
+ * It will assume any target that it applies to will have differences
+ * between the start and end state, regardless of the differences
+ * that actually exist. In other words, it doesn't actually check
+ * any size or position differences or any other property of the view.
+ * It just records the difference.
+ *
+ *
+ * Both start and end value Views are recorded, but no actual animation
+ * is created.
+ */
+class TrackingTransition : Transition(), TargetTracking {
+ val targets = ArrayList<View>()
+ private val baseEpicenter = Rect()
+
+ override fun getTransitionProperties(): Array<String> {
+ return PROPS
+ }
+
+ override fun captureStartValues(transitionValues: TransitionValues) {
+ transitionValues.values[PROP] = 0
+ }
+
+ override fun captureEndValues(transitionValues: TransitionValues) {
+ transitionValues.values[PROP] = 1
+ }
+
+ override fun createAnimator(
+ sceneRoot: ViewGroup,
+ startValues: TransitionValues?,
+ endValues: TransitionValues?
+ ) = null.also {
+ if (startValues != null) {
+ targets.add(startValues.view)
+ }
+ if (endValues != null) {
+ targets.add(endValues.view)
+ }
+ if (epicenter != null) {
+ baseEpicenter.set(Rect(epicenter))
+ }
+ }
+
+ override fun getTrackedTargets(): ArrayList<View> {
+ return targets
+ }
+
+ override fun clearTargets() {
+ targets.clear()
+ }
+
+ override fun getCapturedEpicenter(): Rect? {
+ return baseEpicenter
+ }
+
+ companion object {
+ private val PROP = "tracking:prop"
+ private val PROPS = arrayOf(PROP)
+ }
+}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/TrackingVisibility.java b/fragment/src/androidTest/java/androidx/fragment/app/TrackingVisibility.java
index 3b0dbb1a..2c54f2d 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/TrackingVisibility.java
+++ b/fragment/src/androidTest/java/androidx/fragment/app/TrackingVisibility.java
@@ -28,7 +28,7 @@
* Visibility transition that tracks which targets are applied to it.
* This transition does no animation.
*/
-class TrackingVisibility extends Visibility implements TargetTracking {
+public class TrackingVisibility extends Visibility implements TargetTracking {
public final ArrayList<View> targets = new ArrayList<>();
private final Rect[] mEpicenter = new Rect[1];
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/TransitionFragment.java b/fragment/src/androidTest/java/androidx/fragment/app/TransitionFragment.java
deleted file mode 100644
index ad8b45b1..0000000
--- a/fragment/src/androidTest/java/androidx/fragment/app/TransitionFragment.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * 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 androidx.fragment.app;
-
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.verify;
-
-import android.os.Bundle;
-import android.os.SystemClock;
-import android.transition.Transition;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-/**
- * A fragment that has transitions that can be tracked.
- */
-public class TransitionFragment extends StrictViewFragment {
- public final TrackingVisibility enterTransition = new TrackingVisibility();
- public final TrackingVisibility reenterTransition = new TrackingVisibility();
- public final TrackingVisibility exitTransition = new TrackingVisibility();
- public final TrackingVisibility returnTransition = new TrackingVisibility();
- public final TrackingTransition sharedElementEnter = new TrackingTransition();
- public final TrackingTransition sharedElementReturn = new TrackingTransition();
-
- private Transition.TransitionListener mListener = mock(Transition.TransitionListener.class);
-
- public TransitionFragment() {
- setEnterTransition(enterTransition);
- setReenterTransition(reenterTransition);
- setExitTransition(exitTransition);
- setReturnTransition(returnTransition);
- setSharedElementEnterTransition(sharedElementEnter);
- setSharedElementReturnTransition(sharedElementReturn);
- enterTransition.addListener(mListener);
- sharedElementEnter.addListener(mListener);
- reenterTransition.addListener(mListener);
- exitTransition.addListener(mListener);
- returnTransition.addListener(mListener);
- sharedElementReturn.addListener(mListener);
- }
-
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- checkGetActivity();
- checkState("onCreateView", CREATED);
- mOnCreateViewCalled = true;
- return super.onCreateView(inflater, container, savedInstanceState);
- }
-
- void waitForTransition() throws InterruptedException {
- verify(mListener, CtsMockitoUtils.within(1000)).onTransitionEnd((Transition) any());
- reset(mListener);
- }
-
- void waitForNoTransition() throws InterruptedException {
- SystemClock.sleep(250);
- verify(mListener, never()).onTransitionStart((Transition) any());
- }
-}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/TransitionFragment.kt b/fragment/src/androidTest/java/androidx/fragment/app/TransitionFragment.kt
new file mode 100644
index 0000000..bd20bd9
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/TransitionFragment.kt
@@ -0,0 +1,77 @@
+/*
+ * 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 androidx.fragment.app
+
+import android.os.SystemClock
+import android.transition.Transition
+import androidx.annotation.LayoutRes
+import androidx.fragment.test.R
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+
+/**
+ * A fragment that has transitions that can be tracked.
+ */
+open class TransitionFragment(
+ @LayoutRes contentLayoutId: Int = R.layout.strict_view_fragment
+) : StrictViewFragment(contentLayoutId) {
+ val enterTransition = TrackingVisibility()
+ val reenterTransition = TrackingVisibility()
+ val exitTransition = TrackingVisibility()
+ val returnTransition = TrackingVisibility()
+ val sharedElementEnter = TrackingTransition()
+ val sharedElementReturn = TrackingTransition()
+
+ private val listener = mock(Transition.TransitionListener::class.java)
+
+ init {
+ @Suppress("LeakingThis")
+ setEnterTransition(enterTransition)
+ @Suppress("LeakingThis")
+ setReenterTransition(reenterTransition)
+ @Suppress("LeakingThis")
+ setExitTransition(exitTransition)
+ @Suppress("LeakingThis")
+ setReturnTransition(returnTransition)
+ sharedElementEnterTransition = sharedElementEnter
+ sharedElementReturnTransition = sharedElementReturn
+ enterTransition.addListener(listener)
+ sharedElementEnter.addListener(listener)
+ reenterTransition.addListener(listener)
+ exitTransition.addListener(listener)
+ returnTransition.addListener(listener)
+ sharedElementReturn.addListener(listener)
+ }
+
+ internal fun waitForTransition() {
+ verify(
+ listener,
+ CtsMockitoUtils.within(1000)
+ ).onTransitionEnd(ArgumentMatchers.any())
+ reset(listener)
+ }
+
+ internal fun waitForNoTransition() {
+ SystemClock.sleep(250)
+ verify(
+ listener,
+ never()
+ ).onTransitionStart(ArgumentMatchers.any())
+ }
+}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/test/FragmentTestActivity.java b/fragment/src/androidTest/java/androidx/fragment/app/test/FragmentTestActivity.java
deleted file mode 100644
index 0d7443d..0000000
--- a/fragment/src/androidTest/java/androidx/fragment/app/test/FragmentTestActivity.java
+++ /dev/null
@@ -1,291 +0,0 @@
-/*
- * 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 androidx.fragment.app.test;
-
-import static org.junit.Assert.assertFalse;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Build.VERSION;
-import android.os.Build.VERSION_CODES;
-import android.os.Bundle;
-import android.transition.Transition;
-import android.transition.Transition.TransitionListener;
-import android.transition.TransitionInflater;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.ContentView;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentActivity;
-import androidx.fragment.test.R;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * A simple activity used for Fragment Transitions and lifecycle event ordering
- */
-@ContentView(R.layout.activity_content)
-public class FragmentTestActivity extends FragmentActivity {
- public final CountDownLatch onDestroyLatch = new CountDownLatch(1);
-
- @Override
- public void onCreate(Bundle icicle) {
- super.onCreate(icicle);
- Intent intent = getIntent();
- if (intent != null && intent.getBooleanExtra("finishEarly", false)) {
- finish();
- getSupportFragmentManager().beginTransaction()
- .add(new AssertNotDestroyed(), "not destroyed")
- .commit();
- }
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- onDestroyLatch.countDown();
- }
-
- public static class TestFragment extends Fragment {
- public static final int ENTER = 0;
- public static final int RETURN = 1;
- public static final int EXIT = 2;
- public static final int REENTER = 3;
- public static final int SHARED_ELEMENT_ENTER = 4;
- public static final int SHARED_ELEMENT_RETURN = 5;
- private static final int TRANSITION_COUNT = 6;
-
- private static final String LAYOUT_ID = "layoutId";
- private static final String TRANSITION_KEY = "transition_";
- private int mLayoutId = R.layout.fragment_start;
- private final int[] mTransitionIds = new int[] {
- R.transition.fade,
- R.transition.fade,
- R.transition.fade,
- R.transition.fade,
- R.transition.change_bounds,
- R.transition.change_bounds,
- };
- private final Object[] mListeners = new Object[TRANSITION_COUNT];
-
- public TestFragment() {
- if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
- for (int i = 0; i < TRANSITION_COUNT; i++) {
- mListeners[i] = new TransitionCalledListener();
- }
- }
- }
-
- public static TestFragment create(int layoutId) {
- TestFragment testFragment = new TestFragment();
- testFragment.mLayoutId = layoutId;
- return testFragment;
- }
-
- public void clearTransitions() {
- for (int i = 0; i < TRANSITION_COUNT; i++) {
- mTransitionIds[i] = 0;
- }
- }
-
- public void clearNotifications() {
- for (int i = 0; i < TRANSITION_COUNT; i++) {
- ((TransitionCalledListener)mListeners[i]).startLatch = new CountDownLatch(1);
- ((TransitionCalledListener)mListeners[i]).endLatch = new CountDownLatch(1);
- }
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (savedInstanceState != null) {
- mLayoutId = savedInstanceState.getInt(LAYOUT_ID, mLayoutId);
- for (int i = 0; i < TRANSITION_COUNT; i++) {
- String key = TRANSITION_KEY + i;
- mTransitionIds[i] = savedInstanceState.getInt(key, mTransitionIds[i]);
- }
- }
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putInt(LAYOUT_ID, mLayoutId);
- for (int i = 0; i < TRANSITION_COUNT; i++) {
- String key = TRANSITION_KEY + i;
- outState.putInt(key, mTransitionIds[i]);
- }
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- return inflater.inflate(mLayoutId, container, false);
- }
-
- @SuppressWarnings("deprecation")
- @Override
- public void onAttach(Activity activity) {
- super.onAttach(activity);
- if (VERSION.SDK_INT > VERSION_CODES.KITKAT) {
- setEnterTransition(loadTransition(ENTER));
- setReenterTransition(loadTransition(REENTER));
- setExitTransition(loadTransition(EXIT));
- setReturnTransition(loadTransition(RETURN));
- setSharedElementEnterTransition(loadTransition(SHARED_ELEMENT_ENTER));
- setSharedElementReturnTransition(loadTransition(SHARED_ELEMENT_RETURN));
- }
- }
-
- public boolean wasStartCalled(int transitionKey) {
- return ((TransitionCalledListener)mListeners[transitionKey]).startLatch.getCount() == 0;
- }
-
- public boolean wasEndCalled(int transitionKey) {
- return ((TransitionCalledListener)mListeners[transitionKey]).endLatch.getCount() == 0;
- }
-
- public boolean waitForStart(int transitionKey)
- throws InterruptedException {
- TransitionCalledListener l = ((TransitionCalledListener)mListeners[transitionKey]);
- return l.startLatch.await(500,TimeUnit.MILLISECONDS);
- }
-
- public boolean waitForEnd(int transitionKey)
- throws InterruptedException {
- TransitionCalledListener l = ((TransitionCalledListener)mListeners[transitionKey]);
- return l.endLatch.await(500,TimeUnit.MILLISECONDS);
- }
-
- private Transition loadTransition(int key) {
- final int id = mTransitionIds[key];
- if (id == 0) {
- return null;
- }
- Transition transition = TransitionInflater.from(getActivity()).inflateTransition(id);
- transition.addListener(((TransitionCalledListener)mListeners[key]));
- return transition;
- }
-
- private class TransitionCalledListener implements TransitionListener {
- public CountDownLatch startLatch = new CountDownLatch(1);
- public CountDownLatch endLatch = new CountDownLatch(1);
-
- public TransitionCalledListener() {
- }
-
- @Override
- public void onTransitionStart(Transition transition) {
- startLatch.countDown();
- }
-
- @Override
- public void onTransitionEnd(Transition transition) {
- endLatch.countDown();
- }
-
- @Override
- public void onTransitionCancel(Transition transition) {
- }
-
- @Override
- public void onTransitionPause(Transition transition) {
- }
-
- @Override
- public void onTransitionResume(Transition transition) {
- }
- }
- }
-
- public static class ParentFragment extends Fragment {
- static final String CHILD_FRAGMENT_TAG = "childFragment";
- public boolean wasAttachedInTime;
-
- private boolean mRetainChild;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- ChildFragment f = getChildFragment();
- if (f == null) {
- f = new ChildFragment();
- if (mRetainChild) {
- f.setRetainInstance(true);
- }
- getChildFragmentManager().beginTransaction().add(f, CHILD_FRAGMENT_TAG).commitNow();
- }
- wasAttachedInTime = f.attached;
- }
-
- public ChildFragment getChildFragment() {
- return (ChildFragment) getChildFragmentManager().findFragmentByTag(CHILD_FRAGMENT_TAG);
- }
-
- public void setRetainChildInstance(boolean retainChild) {
- mRetainChild = retainChild;
- }
- }
-
- public static class ChildFragment extends Fragment {
- private OnAttachListener mOnAttachListener;
-
- public boolean attached;
- public boolean onActivityResultCalled;
- public int onActivityResultRequestCode;
- public int onActivityResultResultCode;
-
- @Override
- public void onAttach(Context activity) {
- super.onAttach(activity);
- attached = true;
- if (mOnAttachListener != null) {
- mOnAttachListener.onAttach(activity, this);
- }
- }
-
- public void setOnAttachListener(OnAttachListener listener) {
- mOnAttachListener = listener;
- }
-
- public interface OnAttachListener {
- void onAttach(Context activity, ChildFragment fragment);
- }
-
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- onActivityResultCalled = true;
- onActivityResultRequestCode = requestCode;
- onActivityResultResultCode = resultCode;
- }
- }
-
- public static class AssertNotDestroyed extends Fragment {
- @Override
- public void onActivityCreated(@Nullable Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
- assertFalse(getActivity().isDestroyed());
- }
- }
- }
-}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/test/FragmentTestActivity.kt b/fragment/src/androidTest/java/androidx/fragment/app/test/FragmentTestActivity.kt
new file mode 100644
index 0000000..3ebf7c9
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/test/FragmentTestActivity.kt
@@ -0,0 +1,109 @@
+/*
+ * 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 androidx.fragment.app.test
+
+import android.content.Context
+import android.content.Intent
+import android.os.Build.VERSION
+import android.os.Build.VERSION_CODES
+import android.os.Bundle
+import androidx.annotation.ContentView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import androidx.fragment.test.R
+import org.junit.Assert.assertFalse
+import java.util.concurrent.CountDownLatch
+
+/**
+ * A simple activity used for Fragment Transitions and lifecycle event ordering
+ */
+@ContentView(R.layout.activity_content)
+class FragmentTestActivity : FragmentActivity() {
+ val onDestroyLatch = CountDownLatch(1)
+
+ public override fun onCreate(icicle: Bundle?) {
+ super.onCreate(icicle)
+ if (intent?.getBooleanExtra("finishEarly", false) == true) {
+ finish()
+ supportFragmentManager.beginTransaction()
+ .add(AssertNotDestroyed(), "not destroyed")
+ .commit()
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ onDestroyLatch.countDown()
+ }
+
+ class ParentFragment : Fragment() {
+ var wasAttachedInTime: Boolean = false
+
+ var retainChildInstance: Boolean = false
+
+ val childFragment: ChildFragment
+ get() = childFragmentManager.findFragmentByTag(CHILD_FRAGMENT_TAG) as ChildFragment
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ if (childFragmentManager.findFragmentByTag(CHILD_FRAGMENT_TAG) == null) {
+ childFragmentManager.beginTransaction()
+ .add(ChildFragment().apply {
+ if (retainChildInstance) {
+ retainInstance = true
+ }
+ }, CHILD_FRAGMENT_TAG)
+ .commitNow()
+ }
+ wasAttachedInTime = childFragment.attached
+ }
+
+ companion object {
+ internal const val CHILD_FRAGMENT_TAG = "childFragment"
+ }
+ }
+
+ class ChildFragment : Fragment() {
+ var onAttachListener: (context: Context) -> Unit = {}
+
+ var attached: Boolean = false
+ var onActivityResultCalled: Boolean = false
+ var onActivityResultRequestCode: Int = 0
+ var onActivityResultResultCode: Int = 0
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ attached = true
+ onAttachListener.invoke(context)
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ onActivityResultCalled = true
+ onActivityResultRequestCode = requestCode
+ onActivityResultResultCode = resultCode
+ }
+ }
+
+ class AssertNotDestroyed : Fragment() {
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+ if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
+ assertFalse(requireActivity().isDestroyed)
+ }
+ }
+ }
+}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/test/HangingFragmentActivity.java b/fragment/src/androidTest/java/androidx/fragment/app/test/HangingFragmentActivity.java
deleted file mode 100644
index 971314b..0000000
--- a/fragment/src/androidTest/java/androidx/fragment/app/test/HangingFragmentActivity.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * 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 androidx.fragment.app.test;
-
-import android.os.Bundle;
-
-import androidx.annotation.Nullable;
-import androidx.fragment.test.R;
-import androidx.testutils.RecreatedActivity;
-
-public class HangingFragmentActivity extends RecreatedActivity {
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(savedInstanceState == null ? R.layout.activity_inflated_fragment
- : R.layout.activity_content);
- }
-}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/test/HangingFragmentActivity.kt b/fragment/src/androidTest/java/androidx/fragment/app/test/HangingFragmentActivity.kt
new file mode 100644
index 0000000..39a4aff
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/test/HangingFragmentActivity.kt
@@ -0,0 +1,34 @@
+/*
+ * 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 androidx.fragment.app.test
+
+import android.os.Bundle
+import androidx.fragment.test.R
+import androidx.testutils.RecreatedActivity
+
+class HangingFragmentActivity : RecreatedActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(
+ if (savedInstanceState == null)
+ R.layout.activity_inflated_fragment
+ else
+ R.layout.activity_content
+ )
+ }
+}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/test/LoaderActivity.java b/fragment/src/androidTest/java/androidx/fragment/app/test/LoaderActivity.java
deleted file mode 100644
index 77f4145..0000000
--- a/fragment/src/androidTest/java/androidx/fragment/app/test/LoaderActivity.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * 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 androidx.fragment.app.test;
-
-import android.content.Context;
-import android.os.Bundle;
-import android.view.View;
-import android.widget.TextView;
-
-import androidx.annotation.ContentView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.test.R;
-import androidx.loader.app.LoaderManager;
-import androidx.loader.content.AsyncTaskLoader;
-import androidx.loader.content.Loader;
-import androidx.testutils.RecreatedActivity;
-
-@ContentView(R.layout.activity_loader)
-public class LoaderActivity extends RecreatedActivity
- implements LoaderManager.LoaderCallbacks<String> {
- private static final int TEXT_LOADER_ID = 14;
-
- public TextView textView;
- public TextView textViewB;
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- textView = findViewById(R.id.textA);
- textViewB = findViewById(R.id.textB);
-
- if (savedInstanceState == null) {
- getSupportFragmentManager()
- .beginTransaction()
- .add(R.id.fragmentContainer, new TextLoaderFragment())
- .commit();
- }
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- LoaderManager.getInstance(this).initLoader(TEXT_LOADER_ID, null, this);
- }
-
- @NonNull
- @Override
- public Loader<String> onCreateLoader(int id, @Nullable Bundle args) {
- return new TextLoader(this);
- }
-
- @Override
- public void onLoadFinished(@NonNull Loader<String> loader, String data) {
- textView.setText(data);
- }
-
- @Override
- public void onLoaderReset(@NonNull Loader<String> loader) {
- }
-
- static class TextLoader extends AsyncTaskLoader<String> {
- TextLoader(Context context) {
- super(context);
- }
-
- @Override
- protected void onStartLoading() {
- forceLoad();
- }
-
- @Override
- public String loadInBackground() {
- return "Loaded!";
- }
- }
-
- @ContentView(R.layout.fragment_c)
- public static class TextLoaderFragment extends Fragment
- implements LoaderManager.LoaderCallbacks<String> {
- public TextView textView;
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- LoaderManager.getInstance(this).initLoader(TEXT_LOADER_ID, null, this);
- }
-
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- textView = view.findViewById(R.id.textC);
- }
-
- @NonNull
- @Override
- public Loader<String> onCreateLoader(int id, @Nullable Bundle args) {
- return new TextLoader(getContext());
- }
-
- @Override
- public void onLoadFinished(@NonNull Loader<String> loader, String data) {
- textView.setText(data);
- }
-
- @Override
- public void onLoaderReset(@NonNull Loader<String> loader) {
- }
- }
-}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/test/LoaderActivity.kt b/fragment/src/androidTest/java/androidx/fragment/app/test/LoaderActivity.kt
new file mode 100644
index 0000000..ea44bf9
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/test/LoaderActivity.kt
@@ -0,0 +1,107 @@
+/*
+ * 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 androidx.fragment.app.test
+
+import android.content.Context
+import android.os.Bundle
+import android.view.View
+import android.widget.TextView
+
+import androidx.annotation.ContentView
+import androidx.fragment.app.Fragment
+import androidx.fragment.test.R
+import androidx.loader.app.LoaderManager
+import androidx.loader.content.AsyncTaskLoader
+import androidx.loader.content.Loader
+import androidx.testutils.RecreatedActivity
+
+@ContentView(R.layout.activity_loader)
+class LoaderActivity : RecreatedActivity(), LoaderManager.LoaderCallbacks<String> {
+
+ lateinit var textView: TextView
+ lateinit var textViewB: TextView
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ textView = findViewById(R.id.textA)
+ textViewB = findViewById(R.id.textB)
+
+ if (savedInstanceState == null) {
+ supportFragmentManager
+ .beginTransaction()
+ .add(R.id.fragmentContainer, TextLoaderFragment())
+ .commit()
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ LoaderManager.getInstance(this).initLoader(TEXT_LOADER_ID, null, this)
+ }
+
+ override fun onCreateLoader(id: Int, args: Bundle?): Loader<String> {
+ return TextLoader(this)
+ }
+
+ override fun onLoadFinished(loader: Loader<String>, data: String) {
+ textView.text = data
+ }
+
+ override fun onLoaderReset(loader: Loader<String>) {
+ }
+
+ internal class TextLoader(context: Context) : AsyncTaskLoader<String>(context) {
+
+ override fun onStartLoading() {
+ forceLoad()
+ }
+
+ override fun loadInBackground(): String? {
+ return "Loaded!"
+ }
+ }
+
+ @ContentView(R.layout.fragment_c)
+ class TextLoaderFragment : Fragment(), LoaderManager.LoaderCallbacks<String> {
+ lateinit var textView: TextView
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ LoaderManager.getInstance(this).initLoader(TEXT_LOADER_ID, null, this)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ textView = view.findViewById(R.id.textC)
+ }
+
+ override fun onCreateLoader(id: Int, args: Bundle?): Loader<String> {
+ return TextLoader(requireContext())
+ }
+
+ override fun onLoadFinished(loader: Loader<String>, data: String) {
+ textView.text = data
+ }
+
+ override fun onLoaderReset(loader: Loader<String>) {}
+ }
+
+ companion object {
+ private const val TEXT_LOADER_ID = 14
+
+ val activity get() = RecreatedActivity.activity
+ }
+}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/test/NonConfigOnStopActivity.java b/fragment/src/androidTest/java/androidx/fragment/app/test/NonConfigOnStopActivity.java
deleted file mode 100644
index 7cba3a7..0000000
--- a/fragment/src/androidTest/java/androidx/fragment/app/test/NonConfigOnStopActivity.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * 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 androidx.fragment.app.test;
-
-import androidx.fragment.app.Fragment;
-import androidx.testutils.RecreatedActivity;
-
-public class NonConfigOnStopActivity extends RecreatedActivity {
- @Override
- protected void onStop() {
- super.onStop();
-
- getSupportFragmentManager()
- .beginTransaction()
- .add(new RetainedFragment(), "1")
- .commitNowAllowingStateLoss();
- }
-
- public static class RetainedFragment extends Fragment {
- public RetainedFragment() {
- setRetainInstance(true);
- }
- }
-}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/test/NonConfigOnStopActivity.kt b/fragment/src/androidTest/java/androidx/fragment/app/test/NonConfigOnStopActivity.kt
new file mode 100644
index 0000000..2c011c6
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/test/NonConfigOnStopActivity.kt
@@ -0,0 +1,37 @@
+/*
+ * 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 androidx.fragment.app.test
+
+import androidx.fragment.app.Fragment
+import androidx.testutils.RecreatedActivity
+
+class NonConfigOnStopActivity : RecreatedActivity() {
+ override fun onStop() {
+ super.onStop()
+
+ supportFragmentManager
+ .beginTransaction()
+ .add(RetainedFragment(), "1")
+ .commitNowAllowingStateLoss()
+ }
+
+ class RetainedFragment : Fragment() {
+ init {
+ retainInstance = true
+ }
+ }
+}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/test/OuterPackagePrivateFragment.java b/fragment/src/androidTest/java/androidx/fragment/app/test/OuterPackagePrivateFragment.java
new file mode 100644
index 0000000..63092bc
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/test/OuterPackagePrivateFragment.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2019 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 androidx.fragment.app.test;
+
+import androidx.fragment.app.Fragment;
+
+/**
+ * A class with a static PackagePrivate Fragment.
+ * Used for testing FragmentTransactionTest.
+ *
+ * Must be java, the concept of a static PackagePrivate class does not exist in Kotlin.
+ */
+public class OuterPackagePrivateFragment {
+ static class PackagePrivateFragment extends Fragment {}
+}
diff --git a/fragment/src/main/java/androidx/fragment/app/BackStackRecord.java b/fragment/src/main/java/androidx/fragment/app/BackStackRecord.java
index a9feb93..618c387 100644
--- a/fragment/src/main/java/androidx/fragment/app/BackStackRecord.java
+++ b/fragment/src/main/java/androidx/fragment/app/BackStackRecord.java
@@ -422,7 +422,7 @@
+ name + "' has already been added to the transaction.");
} else if (mSharedElementSourceNames.contains(transitionName)) {
throw new IllegalArgumentException("A shared element with the source name '"
- + transitionName + " has already been added to the transaction.");
+ + transitionName + "' has already been added to the transaction.");
}
mSharedElementSourceNames.add(transitionName);
diff --git a/fragment/src/main/java/androidx/fragment/app/Fragment.java b/fragment/src/main/java/androidx/fragment/app/Fragment.java
index 6e52230..ff82951 100644
--- a/fragment/src/main/java/androidx/fragment/app/Fragment.java
+++ b/fragment/src/main/java/androidx/fragment/app/Fragment.java
@@ -686,8 +686,10 @@
} else if (mFragmentManager != null && fragment.mFragmentManager != null) {
// Just save the reference to the Fragment
mTargetWho = fragment.mWho;
+ mTarget = null;
} else {
// Save the Fragment itself, waiting until we're attached
+ mTargetWho = null;
mTarget = fragment;
}
mTargetRequestCode = requestCode;
diff --git a/fragment/src/main/java/androidx/fragment/app/FragmentTabHost.java b/fragment/src/main/java/androidx/fragment/app/FragmentTabHost.java
index 72b7a20..8f16d90 100644
--- a/fragment/src/main/java/androidx/fragment/app/FragmentTabHost.java
+++ b/fragment/src/main/java/androidx/fragment/app/FragmentTabHost.java
@@ -41,16 +41,10 @@
* the hierarchy you must call {@link #setup(Context, FragmentManager, int)}
* to complete the initialization of the tab host.
*
- * <p>Here is a simple example of using a FragmentTabHost in an Activity:
- *
- * {@sample frameworks/support/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentTabs.java
- * complete}
- *
- * <p>This can also be used inside of a fragment through fragment nesting:
- *
- * {@sample frameworks/support/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentTabsFragmentSupport.java
- * complete}
+ * @deprecated Use <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
+@Deprecated
public class FragmentTabHost extends TabHost
implements TabHost.OnTabChangeListener {
private final ArrayList<TabInfo> mTabs = new ArrayList<>();
@@ -131,6 +125,12 @@
};
}
+ /**
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
+ */
+ @Deprecated
public FragmentTabHost(@NonNull Context context) {
// Note that we call through to the version that takes an AttributeSet,
// because the simple Context construct can result in a broken object!
@@ -138,6 +138,12 @@
initFragmentTabHost(context, null);
}
+ /**
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
+ */
+ @Deprecated
public FragmentTabHost(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initFragmentTabHost(context, attrs);
@@ -181,9 +187,9 @@
}
/**
- * @deprecated Don't call the original TabHost setup, you must instead
- * call {@link #setup(Context, FragmentManager)} or
- * {@link #setup(Context, FragmentManager, int)}.
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Override @Deprecated
public void setup() {
@@ -193,7 +199,12 @@
/**
* Set up the FragmentTabHost to use the given FragmentManager
+ *
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
+ @Deprecated
public void setup(@NonNull Context context, @NonNull FragmentManager manager) {
ensureHierarchy(context); // Ensure views required by super.setup()
super.setup();
@@ -204,7 +215,12 @@
/**
* Set up the FragmentTabHost to use the given FragmentManager
+ *
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
+ @Deprecated
public void setup(@NonNull Context context, @NonNull FragmentManager manager,
int containerId) {
ensureHierarchy(context); // Ensure views required by super.setup()
@@ -232,11 +248,23 @@
}
}
+ /**
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
+ */
+ @Deprecated
@Override
public void setOnTabChangedListener(@Nullable OnTabChangeListener l) {
mOnTabChangeListener = l;
}
+ /**
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
+ */
+ @Deprecated
public void addTab(@NonNull TabHost.TabSpec tabSpec, @NonNull Class<?> clss,
@Nullable Bundle args) {
tabSpec.setContent(new DummyTabFactory(mContext));
@@ -260,6 +288,12 @@
addTab(tabSpec);
}
+ /**
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
+ */
+ @Deprecated
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
@@ -299,12 +333,24 @@
}
}
+ /**
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
+ */
+ @Deprecated
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mAttached = false;
}
+ /**
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
+ */
+ @Deprecated
@Override
@NonNull
protected Parcelable onSaveInstanceState() {
@@ -314,6 +360,12 @@
return ss;
}
+ /**
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
+ */
+ @Deprecated
@Override
protected void onRestoreInstanceState(@SuppressLint("UnknownNullness") Parcelable state) {
if (!(state instanceof SavedState)) {
@@ -325,6 +377,12 @@
setCurrentTabByTag(ss.curTab);
}
+ /**
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
+ */
+ @Deprecated
@Override
public void onTabChanged(@Nullable String tabId) {
if (mAttached) {
diff --git a/graphics/drawable/animated/build.gradle b/graphics/drawable/animated/build.gradle
index 4ce8b2a..fae5204 100644
--- a/graphics/drawable/animated/build.gradle
+++ b/graphics/drawable/animated/build.gradle
@@ -8,7 +8,7 @@
dependencies {
api(project(":vectordrawable"))
- implementation(project(":interpolator"))
+ implementation("androidx.interpolator:interpolator:1.0.0")
implementation(project(":collection"))
androidTestImplementation(TEST_EXT_JUNIT)
diff --git a/jetifier/jetifier/core/src/main/resources/default.config b/jetifier/jetifier/core/src/main/resources/default.config
index c39f546..4a18f8d 100644
--- a/jetifier/jetifier/core/src/main/resources/default.config
+++ b/jetifier/jetifier/core/src/main/resources/default.config
@@ -1002,303 +1002,303 @@
],
"packageMap": [
{
- "from" = "android/support/exifinterface",
- "to" = "androidx/exifinterface"
+ "from" : "android/support/exifinterface",
+ "to" : "androidx/exifinterface"
},
{
- "from" = "android/support/heifwriter",
- "to" = "androidx/heifwriter"
+ "from" : "android/support/heifwriter",
+ "to" : "androidx/heifwriter"
},
{
- "from" = "android/support/graphics/drawable",
- "to" = "androidx/vectordrawable"
+ "from" : "android/support/graphics/drawable",
+ "to" : "androidx/vectordrawable"
},
{
- "from" = "android/support/graphics/drawable/animated",
- "to" = "androidx/vectordrawable"
+ "from" : "android/support/graphics/drawable/animated",
+ "to" : "androidx/vectordrawable"
},
{
- "from" = "android/support/media/tv",
- "to" = "androidx/tvprovider"
+ "from" : "android/support/media/tv",
+ "to" : "androidx/tvprovider"
},
{
- "from" = "android/support/textclassifier",
- "to" = "androidx/textclassifier"
+ "from" : "android/support/textclassifier",
+ "to" : "androidx/textclassifier"
},
{
- "from" = "androidx/recyclerview/selection",
- "to" = "androidx/recyclerview/selection"},
+ "from" : "androidx/recyclerview/selection",
+ "to" : "androidx/recyclerview/selection"},
{
- "from" = "android/support/v4",
- "to" = "androidx/legacy/v4"
+ "from" : "android/support/v4",
+ "to" : "androidx/legacy/v4"
},
{
- "from" = "android/support/print",
- "to" = "androidx/print"
+ "from" : "android/support/print",
+ "to" : "androidx/print"
},
{
- "from" = "android/support/documentfile",
- "to" = "androidx/documentfile"
+ "from" : "android/support/documentfile",
+ "to" : "androidx/documentfile"
},
{
- "from" = "android/support/coordinatorlayout",
- "to" = "androidx/coordinatorlayout"
+ "from" : "android/support/coordinatorlayout",
+ "to" : "androidx/coordinatorlayout"
},
{
- "from" = "android/support/swiperefreshlayout",
- "to" = "androidx/swiperefreshlayout"
+ "from" : "android/support/swiperefreshlayout",
+ "to" : "androidx/swiperefreshlayout"
},
{
- "from" = "android/support/slidingpanelayout",
- "to" = "androidx/slidingpanelayout"
+ "from" : "android/support/slidingpanelayout",
+ "to" : "androidx/slidingpanelayout"
},
{
- "from" = "android/support/asynclayoutinflater",
- "to" = "androidx/asynclayoutinflater"
+ "from" : "android/support/asynclayoutinflater",
+ "to" : "androidx/asynclayoutinflater"
},
{
- "from" = "android/support/interpolator",
- "to" = "androidx/interpolator"
+ "from" : "android/support/interpolator",
+ "to" : "androidx/interpolator"
},
{
- "from" = "android/support/v7/palette",
- "to" = "androidx/palette"
+ "from" : "android/support/v7/palette",
+ "to" : "androidx/palette"
},
{
- "from" = "android/support/v7/cardview",
- "to" = "androidx/cardview"
+ "from" : "android/support/v7/cardview",
+ "to" : "androidx/cardview"
},
{
- "from" = "android/support/customview",
- "to" = "androidx/customview"
+ "from" : "android/support/customview",
+ "to" : "androidx/customview"
},
{
- "from" = "android/support/loader",
- "to" = "androidx/loader"
+ "from" : "android/support/loader",
+ "to" : "androidx/loader"
},
{
- "from" = "android/support/cursoradapter",
- "to" = "androidx/cursoradapter"
+ "from" : "android/support/cursoradapter",
+ "to" : "androidx/cursoradapter"
},
{
- "from" = "android/support/v7/mediarouter",
- "to" = "androidx/mediarouter"
+ "from" : "android/support/v7/mediarouter",
+ "to" : "androidx/mediarouter"
},
{
- "from" = "android/support/v7/appcompat",
- "to" = "androidx/appcompat"
+ "from" : "android/support/v7/appcompat",
+ "to" : "androidx/appcompat"
},
{
- "from" = "android/support/v7/recyclerview",
- "to" = "androidx/recyclerview"
+ "from" : "android/support/v7/recyclerview",
+ "to" : "androidx/recyclerview"
},
{
- "from" = "android/support/v7/viewpager",
- "to" = "androidx/viewpager"
+ "from" : "android/support/v7/viewpager",
+ "to" : "androidx/viewpager"
},
{
- "from" = "android/support/percent",
- "to" = "androidx/percentlayout"
+ "from" : "android/support/percent",
+ "to" : "androidx/percentlayout"
},
{
- "from" = "android/support/v7/gridlayout",
- "to" = "androidx/gridlayout"
+ "from" : "android/support/v7/gridlayout",
+ "to" : "androidx/gridlayout"
},
{
- "from" = "android/support/v13",
- "to" = "androidx/legacy/v13"
+ "from" : "android/support/v13",
+ "to" : "androidx/legacy/v13"
},
{
- "from" = "android/support/v7/preference",
- "to" = "androidx/preference"
+ "from" : "android/support/v7/preference",
+ "to" : "androidx/preference"
},
{
- "from" = "android/support/v14/preference",
- "to" = "androidx/legacy/preference"
+ "from" : "android/support/v14/preference",
+ "to" : "androidx/legacy/preference"
},
{
- "from" = "android/support/v17/leanback",
- "to" = "androidx/leanback"
+ "from" : "android/support/v17/leanback",
+ "to" : "androidx/leanback"
},
{
- "from" = "android/support/v17/preference",
- "to" = "androidx/leanback/preference"
+ "from" : "android/support/v17/preference",
+ "to" : "androidx/leanback/preference"
},
{
- "from" = "android/support/compat",
- "to" = "androidx/core"
+ "from" : "android/support/compat",
+ "to" : "androidx/core"
},
{
- "from" = "android/support/mediacompat",
- "to" = "androidx/media"
+ "from" : "android/support/mediacompat",
+ "to" : "androidx/media"
},
{
- "from" = "android/support/media2",
- "to" = "androidx/media2"
+ "from" : "android/support/media2",
+ "to" : "androidx/media2"
},
{
- "from" = "androidx/media2/exoplayer/external",
- "to" = "androidx/media2/exoplayer/external"
+ "from" : "androidx/media2/exoplayer/external",
+ "to" : "androidx/media2/exoplayer/external"
},
{
- "from" = "android/support/fragment",
- "to" = "androidx/fragment"
+ "from" : "android/support/fragment",
+ "to" : "androidx/fragment"
},
{
- "from" = "android/support/coreutils",
- "to" = "androidx/legacy/coreutils"
+ "from" : "android/support/coreutils",
+ "to" : "androidx/legacy/coreutils"
},
{
- "from" = "android/support/dynamicanimation",
- "to" = "androidx/dynamicanimation"
+ "from" : "android/support/dynamicanimation",
+ "to" : "androidx/dynamicanimation"
},
{
- "from" = "android/support/customtabs",
- "to" = "androidx/browser"
+ "from" : "android/support/customtabs",
+ "to" : "androidx/browser"
},
{
- "from" = "android/support/coreui",
- "to" = "androidx/legacy/coreui"
+ "from" : "android/support/coreui",
+ "to" : "androidx/legacy/coreui"
},
{
- "from" = "android/support/content",
- "to" = "androidx/contentpager"
+ "from" : "android/support/content",
+ "to" : "androidx/contentpager"
},
{
- "from" = "android/support/transition",
- "to" = "androidx/transition"
+ "from" : "android/support/transition",
+ "to" : "androidx/transition"
},
{
- "from" = "android/support/recommendation",
- "to" = "androidx/recommendation"
+ "from" : "android/support/recommendation",
+ "to" : "androidx/recommendation"
},
{
- "from" = "android/support/drawerlayout",
- "to" = "androidx/drawerlayout"
+ "from" : "android/support/drawerlayout",
+ "to" : "androidx/drawerlayout"
},
{
- "from" = "android/support/wear",
- "to" = "androidx/wear"
+ "from" : "android/support/wear",
+ "to" : "androidx/wear"
},
{
- "from" = "android/support/design",
- "to" = "com/google/android/material"
+ "from" : "android/support/design",
+ "to" : "com/google/android/material"
},
{
- "from" = "android/support/text/emoji/appcompat",
- "to" = "androidx/emoji/appcompat"
+ "from" : "android/support/text/emoji/appcompat",
+ "to" : "androidx/emoji/appcompat"
},
{
- "from" = "android/support/text/emoji/bundled",
- "to" = "androidx/emoji/bundled"
+ "from" : "android/support/text/emoji/bundled",
+ "to" : "androidx/emoji/bundled"
},
{
- "from" = "android/support/text/emoji",
- "to" = "androidx/emoji"
+ "from" : "android/support/text/emoji",
+ "to" : "androidx/emoji"
},
{
- "from" = "androidx/text/emoji/bundled",
- "to" = "androidx/text/emoji/bundled"
+ "from" : "androidx/text/emoji/bundled",
+ "to" : "androidx/text/emoji/bundled"
},
{
- "from" = "android/support/localbroadcastmanager",
- "to" = "androidx/localbroadcastmanager"
+ "from" : "android/support/localbroadcastmanager",
+ "to" : "androidx/localbroadcastmanager"
},
{
- "from" = "androidx/text/emoji/bundled",
- "to" = "androidx/text/emoji/bundled"
+ "from" : "androidx/text/emoji/bundled",
+ "to" : "androidx/text/emoji/bundled"
},
{
- "from" = "androidx/webkit",
- "to" = "androidx/webkit"
+ "from" : "androidx/webkit",
+ "to" : "androidx/webkit"
},
{
- "from" = "androidx/versionedparcelable",
- "to" = "androidx/versionedparcelable"
+ "from" : "androidx/versionedparcelable",
+ "to" : "androidx/versionedparcelable"
},
{
- "from" = "androidx/slice/view",
- "to" = "androidx/slice/view"
+ "from" : "androidx/slice/view",
+ "to" : "androidx/slice/view"
},
{
- "from" = "androidx/slice/core",
- "to" = "androidx/slice/core"
+ "from" : "androidx/slice/core",
+ "to" : "androidx/slice/core"
},
{
- "from" = "androidx/slice/builders",
- "to" = "androidx/slice/builders"
+ "from" : "androidx/slice/builders",
+ "to" : "androidx/slice/builders"
},
{
- "from" = "android/arch/paging/runtime",
- "to" = "androidx/paging/runtime"
+ "from" : "android/arch/paging/runtime",
+ "to" : "androidx/paging/runtime"
},
{
- "from" = "android/arch/core/testing",
- "to" = "androidx/arch/core/testing"
+ "from" : "android/arch/core/testing",
+ "to" : "androidx/arch/core/testing"
},
{
- "from" = "android/arch/core",
- "to" = "androidx/arch/core"
+ "from" : "android/arch/core",
+ "to" : "androidx/arch/core"
},
{
- "from" = "android/arch/persistence/db/framework",
- "to" = "androidx/sqlite/db/framework"
+ "from" : "android/arch/persistence/db/framework",
+ "to" : "androidx/sqlite/db/framework"
},
{
- "from" = "android/arch/persistence/db",
- "to" = "androidx/sqlite/db"
+ "from" : "android/arch/persistence/db",
+ "to" : "androidx/sqlite/db"
},
{
- "from" = "android/arch/persistence/room/rxjava2",
- "to" = "androidx/room/rxjava2"
+ "from" : "android/arch/persistence/room/rxjava2",
+ "to" : "androidx/room/rxjava2"
},
{
- "from" = "android/arch/persistence/room/guava",
- "to" = "androidx/room/guava"
+ "from" : "android/arch/persistence/room/guava",
+ "to" : "androidx/room/guava"
},
{
- "from" = "android/arch/persistence/room/testing",
- "to" = "androidx/room/testing"
+ "from" : "android/arch/persistence/room/testing",
+ "to" : "androidx/room/testing"
},
{
- "from" = "android/arch/persistence/room",
- "to" = "androidx/room"
+ "from" : "android/arch/persistence/room",
+ "to" : "androidx/room"
},
{
- "from" = "android/arch/lifecycle/extensions",
- "to" = "androidx/lifecycle/extensions"
+ "from" : "android/arch/lifecycle/extensions",
+ "to" : "androidx/lifecycle/extensions"
},
{
- "from" = "android/arch/lifecycle/livedata/core",
- "to" = "androidx/lifecycle/livedata/core"
+ "from" : "android/arch/lifecycle/livedata/core",
+ "to" : "androidx/lifecycle/livedata/core"
},
{
- "from" = "android/arch/lifecycle",
- "to" = "androidx/lifecycle"
+ "from" : "android/arch/lifecycle",
+ "to" : "androidx/lifecycle"
},
{
- "from" = "android/arch/lifecycle/viewmodel",
- "to" = "androidx/lifecycle/viewmodel"
+ "from" : "android/arch/lifecycle/viewmodel",
+ "to" : "androidx/lifecycle/viewmodel"
},
{
- "from" = "android/arch/lifecycle/livedata",
- "to" = "androidx/lifecycle/livedata"
+ "from" : "android/arch/lifecycle/livedata",
+ "to" : "androidx/lifecycle/livedata"
},
{
- "from" = "android/arch/lifecycle/reactivestreams",
- "to" = "androidx/lifecycle/reactivestreams"
+ "from" : "android/arch/lifecycle/reactivestreams",
+ "to" : "androidx/lifecycle/reactivestreams"
},
{
- "from" = "android/support/multidex/instrumentation",
- "to" = "androidx/multidex/instrumentation"
+ "from" : "android/support/multidex/instrumentation",
+ "to" : "androidx/multidex/instrumentation"
},
{
- "from" = "android/support/multidex",
- "to" = "androidx/multidex"
+ "from" : "android/support/multidex",
+ "to" : "androidx/multidex"
},
{
- "from" = "android/support/biometric",
- "to" = "androidx/biometric"
+ "from" : "android/support/biometric",
+ "to" : "androidx/biometric"
}
],
"pomRules": [
@@ -1534,14 +1534,38 @@
# "from": { "groupId": "android.arch.background.workmanager", "artifactId": "workmanager-firebase", "version": "{newArchVersion}" },
# "to": { "groupId": "androidx.work", "artifactId": "runtime-firebase", "version": "{newArchVersion}" }
#},
- #{
- # "from": { "groupId": "android.arch.navigation", "artifactId": "runtime", "version": "{newArchVersion}" },
- # "to": { "groupId": "androidx.navigation", "artifactId": "navigation-runtime", "version": "{newArchVersion}" }
- #},
- #{
- # "from": { "groupId": "android.arch.navigation", "artifactId": "fragment", "version": "{newArchVersion}" },
- # "to": { "groupId": "androidx.navigation", "artifactId": "navigation-fragment", "version": "{newArchVersion}" }
- #},
+ {
+ "from": { "groupId": "android.arch.navigation", "artifactId": "common", "version": "{oldNavigationVersion}" },
+ "to": { "groupId": "androidx.navigation", "artifactId": "navigation-common", "version": "{newNavigationVersion}" }
+ },
+ {
+ "from": { "groupId": "android.arch.navigation", "artifactId": "common-ktx", "version": "{oldNavigationVersion}" },
+ "to": { "groupId": "androidx.navigation", "artifactId": "navigation-common-ktx", "version": "{newNavigationVersion}" }
+ },
+ {
+ "from": { "groupId": "android.arch.navigation", "artifactId": "fragment", "version": "{oldNavigationVersion}" },
+ "to": { "groupId": "androidx.navigation", "artifactId": "navigation-fragment", "version": "{newNavigationVersion}" }
+ },
+ {
+ "from": { "groupId": "android.arch.navigation", "artifactId": "fragment-ktx", "version": "{oldNavigationVersion}" },
+ "to": { "groupId": "androidx.navigation", "artifactId": "navigation-fragment-ktx", "version": "{newNavigationVersion}" }
+ },
+ {
+ "from": { "groupId": "android.arch.navigation", "artifactId": "runtime", "version": "{oldNavigationVersion}" },
+ "to": { "groupId": "androidx.navigation", "artifactId": "navigation-runtime", "version": "{newNavigationVersion}" }
+ },
+ {
+ "from": { "groupId": "android.arch.navigation", "artifactId": "runtime-ktx", "version": "{oldNavigationVersion}" },
+ "to": { "groupId": "androidx.navigation", "artifactId": "navigation-runtime-ktx", "version": "{newNavigationVersion}" }
+ },
+ {
+ "from": { "groupId": "android.arch.navigation", "artifactId": "ui", "version": "{oldNavigationVersion}" },
+ "to": { "groupId": "androidx.navigation", "artifactId": "navigation-ui", "version": "{newNavigationVersion}" }
+ },
+ {
+ "from": { "groupId": "android.arch.navigation", "artifactId": "ui-ktx", "version": "{oldNavigationVersion}" },
+ "to": { "groupId": "androidx.navigation", "artifactId": "navigation-ui-ktx", "version": "{newNavigationVersion}" }
+ },
{
"from": { "groupId": "android.arch.core", "artifactId": "common", "version": "1.1.1" },
"to": { "groupId": "androidx.arch.core", "artifactId": "core-common", "version": "{newArchCoreVersion}" }
@@ -1770,6 +1794,7 @@
"oldMedia2Version": "28.0.0-alpha03",
"oldExoplayerVersion": "28.0.0-alpha01",
"oldBiometricVersion": "28.0.0-alpha03",
+ "oldNavigationVersion": "1.0.0",
"newSlVersion": "1.0.0",
"newMaterialVersion": "1.0.0",
"newArchCoreVersion": "2.0.0",
@@ -1785,7 +1810,8 @@
"newMedia2Version": "1.0.0-alpha03",
"newExoplayerVersion": "1.0.0-alpha01",
"newBiometricVersion": "1.0.0-alpha03",
- "newDataBindingVersion": "undefined"
+ "newDataBindingVersion": "undefined",
+ "newNavigationVersion": "2.0.0"
}
},
# Manual fallback types map
diff --git a/jetifier/jetifier/core/src/main/resources/default.generated.config b/jetifier/jetifier/core/src/main/resources/default.generated.config
index 7c257f0..fd6d282 100644
--- a/jetifier/jetifier/core/src/main/resources/default.generated.config
+++ b/jetifier/jetifier/core/src/main/resources/default.generated.config
@@ -1928,6 +1928,102 @@
},
{
"from": {
+ "groupId": "android.arch.navigation",
+ "artifactId": "common",
+ "version": "{oldNavigationVersion}"
+ },
+ "to": {
+ "groupId": "androidx.navigation",
+ "artifactId": "navigation-common",
+ "version": "{newNavigationVersion}"
+ }
+ },
+ {
+ "from": {
+ "groupId": "android.arch.navigation",
+ "artifactId": "common-ktx",
+ "version": "{oldNavigationVersion}"
+ },
+ "to": {
+ "groupId": "androidx.navigation",
+ "artifactId": "navigation-common-ktx",
+ "version": "{newNavigationVersion}"
+ }
+ },
+ {
+ "from": {
+ "groupId": "android.arch.navigation",
+ "artifactId": "fragment",
+ "version": "{oldNavigationVersion}"
+ },
+ "to": {
+ "groupId": "androidx.navigation",
+ "artifactId": "navigation-fragment",
+ "version": "{newNavigationVersion}"
+ }
+ },
+ {
+ "from": {
+ "groupId": "android.arch.navigation",
+ "artifactId": "fragment-ktx",
+ "version": "{oldNavigationVersion}"
+ },
+ "to": {
+ "groupId": "androidx.navigation",
+ "artifactId": "navigation-fragment-ktx",
+ "version": "{newNavigationVersion}"
+ }
+ },
+ {
+ "from": {
+ "groupId": "android.arch.navigation",
+ "artifactId": "runtime",
+ "version": "{oldNavigationVersion}"
+ },
+ "to": {
+ "groupId": "androidx.navigation",
+ "artifactId": "navigation-runtime",
+ "version": "{newNavigationVersion}"
+ }
+ },
+ {
+ "from": {
+ "groupId": "android.arch.navigation",
+ "artifactId": "runtime-ktx",
+ "version": "{oldNavigationVersion}"
+ },
+ "to": {
+ "groupId": "androidx.navigation",
+ "artifactId": "navigation-runtime-ktx",
+ "version": "{newNavigationVersion}"
+ }
+ },
+ {
+ "from": {
+ "groupId": "android.arch.navigation",
+ "artifactId": "ui",
+ "version": "{oldNavigationVersion}"
+ },
+ "to": {
+ "groupId": "androidx.navigation",
+ "artifactId": "navigation-ui",
+ "version": "{newNavigationVersion}"
+ }
+ },
+ {
+ "from": {
+ "groupId": "android.arch.navigation",
+ "artifactId": "ui-ktx",
+ "version": "{oldNavigationVersion}"
+ },
+ "to": {
+ "groupId": "androidx.navigation",
+ "artifactId": "navigation-ui-ktx",
+ "version": "{newNavigationVersion}"
+ }
+ },
+ {
+ "from": {
"groupId": "android.arch.core",
"artifactId": "common",
"version": "1.1.1"
@@ -2561,6 +2657,7 @@
"oldMedia2Version": "28.0.0-alpha03",
"oldExoplayerVersion": "28.0.0-alpha01",
"oldBiometricVersion": "28.0.0-alpha03",
+ "oldNavigationVersion": "1.0.0",
"newSlVersion": "1.0.0",
"newMaterialVersion": "1.0.0",
"newArchCoreVersion": "2.0.0",
@@ -2576,7 +2673,8 @@
"newMedia2Version": "1.0.0-alpha03",
"newExoplayerVersion": "1.0.0-alpha01",
"newBiometricVersion": "1.0.0-alpha03",
- "newDataBindingVersion": "undefined"
+ "newDataBindingVersion": "undefined",
+ "newNavigationVersion": "2.0.0"
}
},
"map": {
diff --git a/legacy/v13/src/main/java/androidx/legacy/app/FragmentTabHost.java b/legacy/v13/src/main/java/androidx/legacy/app/FragmentTabHost.java
index 98de1bb..09f23eb 100644
--- a/legacy/v13/src/main/java/androidx/legacy/app/FragmentTabHost.java
+++ b/legacy/v13/src/main/java/androidx/legacy/app/FragmentTabHost.java
@@ -39,7 +39,8 @@
* used with the platform {@link android.app.Fragment} APIs. You will not
* normally use this, instead using action bar tabs.
*
- * @deprecated Use {@link androidx.fragment.app.FragmentTabHost} instead.
+ * @deprecated Use <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Deprecated
public class FragmentTabHost extends TabHost implements TabHost.OnTabChangeListener {
@@ -121,7 +122,9 @@
}
/**
- * @deprecated Use {@link androidx.fragment.app.FragmentTabHost} instead.
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Deprecated
public FragmentTabHost(Context context) {
@@ -132,7 +135,9 @@
}
/**
- * @deprecated Use {@link androidx.fragment.app.FragmentTabHost} instead.
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Deprecated
public FragmentTabHost(Context context, AttributeSet attrs) {
@@ -178,7 +183,9 @@
}
/**
- * @deprecated Use {@link androidx.fragment.app.FragmentTabHost} instead.
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Override
@Deprecated
@@ -188,7 +195,9 @@
}
/**
- * @deprecated Use {@link androidx.fragment.app.FragmentTabHost} instead.
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Deprecated
public void setup(Context context, FragmentManager manager) {
@@ -200,7 +209,9 @@
}
/**
- * @deprecated Use {@link androidx.fragment.app.FragmentTabHost} instead.
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Deprecated
public void setup(Context context, FragmentManager manager, int containerId) {
@@ -230,7 +241,9 @@
}
/**
- * @deprecated Use {@link androidx.fragment.app.FragmentTabHost} instead.
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Deprecated
@Override
@@ -239,7 +252,9 @@
}
/**
- * @deprecated Use {@link androidx.fragment.app.FragmentTabHost} instead.
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Deprecated
public void addTab(TabHost.TabSpec tabSpec, Class<?> clss, Bundle args) {
@@ -265,7 +280,9 @@
}
/**
- * @deprecated Use {@link androidx.fragment.app.FragmentTabHost} instead.
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Deprecated
@Override
@@ -308,7 +325,9 @@
}
/**
- * @deprecated Use {@link androidx.fragment.app.FragmentTabHost} instead.
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Deprecated
@Override
@@ -318,7 +337,9 @@
}
/**
- * @deprecated Use {@link androidx.fragment.app.FragmentTabHost} instead.
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Deprecated
@Override
@@ -330,7 +351,9 @@
}
/**
- * @deprecated Use {@link androidx.fragment.app.FragmentTabHost} instead.
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Deprecated
@Override
@@ -345,7 +368,9 @@
}
/**
- * @deprecated Use {@link androidx.fragment.app.FragmentTabHost} instead.
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Deprecated
@Override
diff --git a/lifecycle/runtime/eap/README.md b/lifecycle/runtime/eap/README.md
new file mode 100644
index 0000000..1f4c9e7
--- /dev/null
+++ b/lifecycle/runtime/eap/README.md
@@ -0,0 +1,2 @@
+This is a temporary project for the coroutines EAP.
+Contents of this folder will be merged into ktx.
diff --git a/lifecycle/runtime/eap/api/1.0.0-alpha01.txt b/lifecycle/runtime/eap/api/1.0.0-alpha01.txt
new file mode 100644
index 0000000..09e9ab4
--- /dev/null
+++ b/lifecycle/runtime/eap/api/1.0.0-alpha01.txt
@@ -0,0 +1,16 @@
+// Signature format: 3.0
+package androidx.lifecycle {
+
+ public final class PausingDispatcherKt {
+ ctor public PausingDispatcherKt();
+ method public static suspend <T> Object? whenCreated(androidx.lifecycle.LifecycleOwner, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.experimental.Continuation<? super T>,?> block, kotlin.coroutines.experimental.Continuation<? super T> p);
+ method public static suspend <T> Object? whenCreated(androidx.lifecycle.Lifecycle, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.experimental.Continuation<? super T>,?> block, kotlin.coroutines.experimental.Continuation<? super T> p);
+ method public static suspend <T> Object? whenResumed(androidx.lifecycle.LifecycleOwner, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.experimental.Continuation<? super T>,?> block, kotlin.coroutines.experimental.Continuation<? super T> p);
+ method public static suspend <T> Object? whenResumed(androidx.lifecycle.Lifecycle, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.experimental.Continuation<? super T>,?> block, kotlin.coroutines.experimental.Continuation<? super T> p);
+ method public static suspend <T> Object? whenStarted(androidx.lifecycle.LifecycleOwner, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.experimental.Continuation<? super T>,?> block, kotlin.coroutines.experimental.Continuation<? super T> p);
+ method public static suspend <T> Object? whenStarted(androidx.lifecycle.Lifecycle, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.experimental.Continuation<? super T>,?> block, kotlin.coroutines.experimental.Continuation<? super T> p);
+ method public static suspend <T> Object? whenStateAtLeast(androidx.lifecycle.Lifecycle, androidx.lifecycle.Lifecycle.State minState, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.experimental.Continuation<? super T>,?> block, kotlin.coroutines.experimental.Continuation<? super error.NonExistentClass> p);
+ }
+
+}
+
diff --git a/lifecycle/runtime/eap/api/current.txt b/lifecycle/runtime/eap/api/current.txt
new file mode 100644
index 0000000..09e9ab4
--- /dev/null
+++ b/lifecycle/runtime/eap/api/current.txt
@@ -0,0 +1,16 @@
+// Signature format: 3.0
+package androidx.lifecycle {
+
+ public final class PausingDispatcherKt {
+ ctor public PausingDispatcherKt();
+ method public static suspend <T> Object? whenCreated(androidx.lifecycle.LifecycleOwner, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.experimental.Continuation<? super T>,?> block, kotlin.coroutines.experimental.Continuation<? super T> p);
+ method public static suspend <T> Object? whenCreated(androidx.lifecycle.Lifecycle, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.experimental.Continuation<? super T>,?> block, kotlin.coroutines.experimental.Continuation<? super T> p);
+ method public static suspend <T> Object? whenResumed(androidx.lifecycle.LifecycleOwner, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.experimental.Continuation<? super T>,?> block, kotlin.coroutines.experimental.Continuation<? super T> p);
+ method public static suspend <T> Object? whenResumed(androidx.lifecycle.Lifecycle, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.experimental.Continuation<? super T>,?> block, kotlin.coroutines.experimental.Continuation<? super T> p);
+ method public static suspend <T> Object? whenStarted(androidx.lifecycle.LifecycleOwner, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.experimental.Continuation<? super T>,?> block, kotlin.coroutines.experimental.Continuation<? super T> p);
+ method public static suspend <T> Object? whenStarted(androidx.lifecycle.Lifecycle, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.experimental.Continuation<? super T>,?> block, kotlin.coroutines.experimental.Continuation<? super T> p);
+ method public static suspend <T> Object? whenStateAtLeast(androidx.lifecycle.Lifecycle, androidx.lifecycle.Lifecycle.State minState, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.experimental.Continuation<? super T>,?> block, kotlin.coroutines.experimental.Continuation<? super error.NonExistentClass> p);
+ }
+
+}
+
diff --git a/lifecycle/runtime/eap/api/res-1.0.0-alpha01.txt b/lifecycle/runtime/eap/api/res-1.0.0-alpha01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lifecycle/runtime/eap/api/res-1.0.0-alpha01.txt
diff --git a/lifecycle/runtime/eap/api/restricted_1.0.0-alpha01.txt b/lifecycle/runtime/eap/api/restricted_1.0.0-alpha01.txt
new file mode 100644
index 0000000..da4f6cc
--- /dev/null
+++ b/lifecycle/runtime/eap/api/restricted_1.0.0-alpha01.txt
@@ -0,0 +1 @@
+// Signature format: 3.0
diff --git a/lifecycle/runtime/eap/api/restricted_current.txt b/lifecycle/runtime/eap/api/restricted_current.txt
new file mode 100644
index 0000000..da4f6cc
--- /dev/null
+++ b/lifecycle/runtime/eap/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 3.0
diff --git a/lifecycle/runtime/eap/build.gradle b/lifecycle/runtime/eap/build.gradle
new file mode 100644
index 0000000..2ce5f78
--- /dev/null
+++ b/lifecycle/runtime/eap/build.gradle
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2019 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+
+plugins {
+ id("SupportAndroidLibraryPlugin")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ buildTypes {
+ debug {
+ testCoverageEnabled = false // Breaks Kotlin compiler.
+ }
+ }
+}
+
+dependencies {
+ api(project(":lifecycle:lifecycle-common-eap"))
+ api(project(":lifecycle:lifecycle-common"))
+ api(KOTLIN_STDLIB)
+ api(KOTLIN_COROUTINES)
+ implementation(SUPPORT_ANNOTATIONS)
+
+ testImplementation(JUNIT)
+ testImplementation(TEST_EXT_JUNIT)
+ testImplementation(TEST_CORE)
+ testImplementation(TEST_RUNNER)
+ testImplementation(TRUTH)
+
+ androidTestImplementation project(':lifecycle:lifecycle-runtime')
+ androidTestImplementation(TRUTH)
+ androidTestImplementation(TEST_EXT_JUNIT)
+ androidTestImplementation(TEST_CORE)
+ androidTestImplementation(TEST_RUNNER)
+ androidTestImplementation(KOTLIN_COROUTINES_TEST)
+
+}
+
+supportLibrary {
+ name = "Android Lifecycle Kotlin Extensions"
+ publish = false
+ mavenVersion = LibraryVersions.LIFECYCLES_COROUTINES
+ mavenGroup = LibraryGroups.LIFECYCLE
+ inceptionYear = "2019"
+ description = "Kotlin extensions for 'lifecycle' artifact"
+ useMetalava = true
+}
\ No newline at end of file
diff --git a/lifecycle/runtime/eap/src/androidTest/java/androidx/lifecycle/Expectations.kt b/lifecycle/runtime/eap/src/androidTest/java/androidx/lifecycle/Expectations.kt
new file mode 100644
index 0000000..395a840
--- /dev/null
+++ b/lifecycle/runtime/eap/src/androidTest/java/androidx/lifecycle/Expectations.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2019 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 androidx.lifecycle
+
+import com.google.common.truth.Truth
+import java.util.concurrent.atomic.AtomicInteger
+
+/**
+ * Partial copy from
+ * https://github.com/Kotlin/kotlinx.coroutines/blob/master/core/kotlinx-coroutines-core/test/TestBase.kt
+ * to track execution order.
+ */
+class Expectations {
+ private var counter = AtomicInteger(0)
+
+ fun expect(expected: Int) {
+ val order = counter.incrementAndGet()
+ Truth.assertThat(order).isEqualTo(expected)
+ }
+
+ fun expectUnreached() {
+ throw AssertionError("should've not reached here")
+ }
+
+ fun expectTotal(total: Int) {
+ Truth.assertThat(counter.get()).isEqualTo(total)
+ }
+}
\ No newline at end of file
diff --git a/lifecycle/runtime/eap/src/androidTest/java/androidx/lifecycle/FakeLifecycleOwner.kt b/lifecycle/runtime/eap/src/androidTest/java/androidx/lifecycle/FakeLifecycleOwner.kt
new file mode 100644
index 0000000..cd93f87
--- /dev/null
+++ b/lifecycle/runtime/eap/src/androidTest/java/androidx/lifecycle/FakeLifecycleOwner.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2019 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 androidx.lifecycle
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeoutOrNull
+
+class FakeLifecycleOwner(initialState: Lifecycle.State? = null) : LifecycleOwner {
+ private val registry: LifecycleRegistry = LifecycleRegistry(this)
+
+ init {
+ initialState?.let {
+ setState(it)
+ }
+ }
+
+ override fun getLifecycle(): Lifecycle = registry
+
+ fun setState(state: Lifecycle.State) {
+ registry.markState(state)
+ }
+
+ suspend fun awaitExactObserverCount(count: Int, timeout: Long = 1000L): Boolean =
+ // just give job some time to start
+ withTimeoutOrNull(timeout) {
+ while (getObserverCount(count) != count) {
+ delay(50)
+ }
+ true
+ } ?: false
+
+ private suspend fun getObserverCount(count: Int): Int {
+ return withContext(Dispatchers.Main) {
+ registry.observerCount
+ }
+ }
+}
\ No newline at end of file
diff --git a/lifecycle/runtime/eap/src/androidTest/java/androidx/lifecycle/PausingDispatcherTest.kt b/lifecycle/runtime/eap/src/androidTest/java/androidx/lifecycle/PausingDispatcherTest.kt
new file mode 100644
index 0000000..ed241df
--- /dev/null
+++ b/lifecycle/runtime/eap/src/androidTest/java/androidx/lifecycle/PausingDispatcherTest.kt
@@ -0,0 +1,511 @@
+/*
+ * Copyright 2019 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 androidx.lifecycle
+
+import android.util.Log
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.InternalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.async
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.setMain
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeoutOrNull
+import kotlinx.coroutines.yield
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+
+@InternalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PausingDispatcherTest {
+ // TODO update custom dispatchers with the new TestCoroutineContext once available
+ // https://github.com/Kotlin/kotlinx.coroutines/pull/890
+ private val taskTracker = TaskTracker()
+ // track uncaught exceptions on the test scope
+ private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
+ testError = testError ?: throwable
+ }
+ // did we hit any error in the test scope
+ private var testError: Throwable? = null
+ // the executor for the testing scope that uses a different thread pool
+ private val testExecutor = TrackedExecutor(taskTracker, Executors.newFixedThreadPool(4))
+ private val testingScope =
+ CoroutineScope(testExecutor.asCoroutineDispatcher() + Job(null) + exceptionHandler)
+ private val owner = FakeLifecycleOwner(Lifecycle.State.RESUMED)
+ private val mainExecutor = TrackedExecutor(taskTracker, Executors.newSingleThreadExecutor())
+ // tracks execution order
+ private val expectations = Expectations()
+ private lateinit var mainThread: Thread
+
+ @ExperimentalCoroutinesApi
+ @Before
+ fun updateMainHandlerAndDispatcher() {
+ Dispatchers.setMain(mainExecutor.asCoroutineDispatcher())
+ runBlocking(Dispatchers.Main) {
+ // extract the main thread to field for assertions
+ mainThread = Thread.currentThread()
+ }
+ }
+
+ @ExperimentalCoroutinesApi
+ @After
+ fun clearHandlerAndDispatcher() {
+ waitTestingScopeChildren()
+ assertThat(mainExecutor.shutdown(10, TimeUnit.SECONDS)).isTrue()
+ assertThat(testExecutor.shutdown(10, TimeUnit.SECONDS)).isTrue()
+ assertThat(taskTracker.awaitIdle(10, TimeUnit.SECONDS)).isTrue()
+ Dispatchers.resetMain()
+ }
+
+ /**
+ * Ensure nothing in the testing scope is left behind w/o assertions
+ */
+ private fun waitTestingScopeChildren() {
+ runBlocking {
+ val testJob = testingScope.coroutineContext[Job]!!
+ do {
+ val children = testJob.children.toList()
+ assertThat(children.all {
+ withTimeoutOrNull(10_000) {
+ it.join()
+ true
+ } ?: false
+ })
+ } while (children.isNotEmpty())
+ assertThat(testJob.isActive)
+ assertThat(testError).isNull()
+ }
+ }
+
+ @Test
+ fun basic() {
+ val result = runBlocking {
+ owner.whenResumed {
+ assertThread()
+ 3
+ }
+ }
+ assertThat(result).isEqualTo(3)
+ }
+
+ @Test
+ fun yieldTest() {
+ runBlocking(Dispatchers.Main) {
+ owner.whenResumed {
+ expectations.expect(1)
+ launch {
+ expectations.expect(3)
+ yield()
+ expectations.expect(5)
+ }
+ expectations.expect(2)
+ launch {
+ expectations.expect(4)
+ }
+ }
+ expectations.expectTotal(5)
+ }
+ }
+
+ @Test
+ fun runInsideMain() {
+ val res = runBlocking(Dispatchers.Main) {
+ owner.whenResumed {
+ 2
+ }
+ }
+ assertThat(res).isEqualTo(2)
+ }
+
+ @Test
+ fun moveToAnotherDispatcher() {
+ val result = runBlocking {
+ owner.whenResumed {
+ assertThread()
+ val innerResult = withContext(testingScope.coroutineContext) {
+ log("running inner")
+ "hello"
+ }
+ assertThread()
+ log("received inner result $innerResult")
+ innerResult + innerResult
+ }
+ }
+ assertThat(result).isEqualTo("hellohello")
+ }
+
+ @Test
+ fun cancel() {
+ runBlocking {
+ val job = testingScope.launch {
+ owner.whenResumed {
+ try {
+ expectations.expect(1)
+ delay(5000)
+ expectations.expectUnreached()
+ } finally {
+ expectations.expect(2)
+ }
+ }
+ }
+ drain()
+ expectations.expectTotal(1)
+ job.cancelAndJoin()
+ expectations.expectTotal(2)
+ }
+ }
+
+ @Test
+ fun throwException_thenRunAnother() {
+ runBlocking(testingScope.coroutineContext) {
+ try {
+ owner.whenResumed {
+ assertThread()
+ expectations.expect(1)
+ throw IllegalArgumentException(" fail")
+ }
+ @Suppress("UNREACHABLE_CODE")
+ expectations.expectUnreached()
+ } catch (ignored: IllegalArgumentException) {
+ }
+ owner.whenResumed {
+ expectations.expect(2)
+ }
+ }
+ expectations.expectTotal(2)
+ }
+
+ @Test
+ fun innerThrowException() {
+ runBlocking {
+ val job = testingScope.launch {
+ val res = runCatching {
+ owner.whenResumed {
+ try {
+ expectations.expect(1)
+ withContext(testingScope.coroutineContext) {
+ throw IllegalStateException("i fail")
+ }
+ @Suppress("UNREACHABLE_CODE")
+ expectations.expectUnreached()
+ } finally {
+ expectations.expect(2)
+ }
+ @Suppress("UNREACHABLE_CODE")
+ expectations.expectUnreached()
+ }
+ }
+ assertThat(res.exceptionOrNull()).hasMessageThat().isEqualTo("i fail")
+ }
+ job.join()
+ expectations.expectTotal(2)
+ }
+ }
+
+ @Test
+ fun pause_thenResume() {
+ pause()
+ runBlocking {
+ val job = testingScope.launch {
+ owner.whenResumed {
+ expectations.expect(1)
+ }
+ }
+ drain()
+ expectations.expectTotal(0)
+ resume()
+ job.join()
+ expectations.expectTotal(1)
+ }
+ }
+
+ @Test
+ fun pause_thenFinish() {
+ pause()
+ runBlocking {
+ val job = testingScope.launch {
+ owner.whenResumed {
+ try {
+ expectations.expectUnreached()
+ } finally {
+ expectations.expectUnreached()
+ }
+ }
+ }
+ drain()
+ expectations.expectTotal(0)
+ finish()
+ job.join()
+ // never started so shouldn't run finally either
+ expectations.expectTotal(0)
+ }
+ }
+
+ @Test
+ fun finishWhileDelayed() {
+ runBlocking {
+ val job = testingScope.launch {
+ owner.whenResumed {
+ try {
+ expectations.expect(1)
+ delay(100000)
+ expectations.expectUnreached()
+ } finally {
+ expectations.expect(2)
+ assertThat(isActive).isFalse()
+ }
+ }
+ }
+ drain()
+ expectations.expectTotal(1)
+ finish()
+ job.join()
+ expectations.expectTotal(2)
+ }
+ }
+
+ @Test
+ fun innerScopeFailure() {
+ runBlocking {
+ owner.whenResumed {
+ val error = CompletableDeferred<Throwable>()
+ val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
+ error.complete(throwable)
+ }
+ launch(Job() + exceptionHandler) {
+ throw IllegalStateException("i fail")
+ }
+ val a2 = async {
+ expectations.expect(1)
+ }
+ assertThat(error.await()).hasMessageThat().contains("i fail")
+ a2.await()
+ }
+ expectations.expectTotal(1)
+ }
+ }
+
+ @Test
+ fun alreadyFinished() {
+ runBlocking {
+ finish()
+ launch {
+ owner.whenResumed {
+ expectations.expectUnreached()
+ }
+ }.join()
+ expectations.expectTotal(0)
+ }
+ }
+
+ @Test
+ fun catchFinishWhileDelayed() {
+ runBlocking {
+ val job = testingScope.launch {
+ owner.whenResumed {
+ try {
+ expectations.expect(1)
+ delay(100000)
+ expectations.expectUnreached()
+ } catch (e: Exception) {
+ expectations.expect(2)
+ assertThat(isActive).isFalse()
+ } finally {
+ expectations.expect(3)
+ }
+ expectations.expect(4)
+ }
+ }
+ drain()
+ expectations.expectTotal(1)
+ finish()
+ job.join()
+ expectations.expectTotal(4)
+ }
+ }
+
+ @Test
+ fun pauseThenContinue() {
+ runBlocking {
+ val job = testingScope.launch {
+ owner.whenResumed {
+ expectations.expect(1)
+ withContext(testingScope.coroutineContext) {
+ pause()
+ }
+ expectations.expect(2)
+ }
+ expectations.expect(3)
+ }
+ drain()
+ expectations.expectTotal(1)
+ assertThat(owner.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ resume()
+ job.join()
+ expectations.expectTotal(3)
+ }
+ }
+
+ @Test
+ fun parentJobCancelled() {
+ runBlocking {
+ val parent = testingScope.launch {
+ owner.whenResumed {
+ try {
+ expectations.expect(1)
+ delay(5000)
+ expectations.expectUnreached()
+ } finally {
+ expectations.expect(2)
+ }
+ }
+ }
+ drain()
+ expectations.expectTotal(1)
+ parent.cancelAndJoin()
+ expectations.expectTotal(2)
+ }
+ }
+
+ @Test
+ fun innerJobCancelsParent() {
+ try {
+ runBlocking(testingScope.coroutineContext) {
+ owner.whenResumed {
+ throw IllegalStateException("i fail")
+ }
+ }
+ @Suppress("UNREACHABLE_CODE")
+ expectations.expectUnreached()
+ } catch (ex: IllegalStateException) {
+ assertThat(ex).hasMessageThat().isEqualTo("i fail")
+ }
+ }
+
+ @Test
+ fun lifecycleInsideLifecycle() {
+ runBlocking {
+ owner.whenResumed {
+ assertThread()
+ expectations.expect(1)
+ owner.whenResumed {
+ assertThread()
+ expectations.expect(2)
+ }
+ }
+ }
+ expectations.expectTotal(2)
+ }
+
+ @Test
+ fun lifecycleInsideLifecycle_innerFails() {
+ runBlocking {
+ val res = runCatching {
+ owner.whenResumed {
+ try {
+ assertThread()
+ expectations.expect(1)
+ owner.whenResumed {
+ assertThread()
+ expectations.expect(2)
+ try {
+ withContext(testingScope.coroutineContext) {
+ throw IllegalStateException("i fail")
+ }
+ @Suppress("UNREACHABLE_CODE")
+ expectations.expectUnreached()
+ } finally {
+ expectations.expect(3)
+ }
+ }
+ expectations.expectUnreached()
+ } finally {
+ expectations.expect(4)
+ }
+ }
+ }
+ assertThat(res.exceptionOrNull()).hasMessageThat().matches("i fail")
+ }
+ expectations.expectTotal(4)
+ }
+
+ @Test
+ fun cancelInnerCoroutine() {
+ runBlocking {
+ val job = launch {
+ owner.whenResumed {
+ withContext(testingScope.coroutineContext) {
+ delay(200_000)
+ expectations.expectUnreached()
+ }
+ expectations.expectUnreached()
+ }
+ }
+ job.cancelAndJoin()
+ expectations.expectTotal(0)
+ }
+ }
+
+ private fun pause() {
+ runBlocking(Dispatchers.Main) {
+ owner.setState(Lifecycle.State.STARTED)
+ }
+ }
+
+ private fun finish() {
+ runBlocking(Dispatchers.Main) {
+ owner.setState(Lifecycle.State.DESTROYED)
+ }
+ }
+
+ private fun resume() {
+ runBlocking(Dispatchers.Main) {
+ owner.setState(Lifecycle.State.RESUMED)
+ }
+ }
+
+ private fun assertThread() {
+ log("asserting looper")
+ assertThat(Thread.currentThread()).isSameAs(mainThread)
+ }
+
+ private fun log(msg: Any?) {
+ Log.d("TEST-RUN", "[${Thread.currentThread().name}] $msg")
+ }
+
+ private fun drain() {
+ assertThat(taskTracker.awaitIdle(10, TimeUnit.SECONDS)).isTrue()
+ }
+}
\ No newline at end of file
diff --git a/lifecycle/runtime/eap/src/androidTest/java/androidx/lifecycle/TaskTracker.kt b/lifecycle/runtime/eap/src/androidTest/java/androidx/lifecycle/TaskTracker.kt
new file mode 100644
index 0000000..f56e505
--- /dev/null
+++ b/lifecycle/runtime/eap/src/androidTest/java/androidx/lifecycle/TaskTracker.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2019 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 androidx.lifecycle
+
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
+
+/**
+ * A simple counter on which we can await 0.
+ */
+class TaskTracker : TrackedExecutor.Callback {
+ private val lock = ReentrantLock()
+ private val idle = lock.newCondition()
+ private var counter = 0
+
+ override fun inc() {
+ lock.withLock {
+ counter++
+ }
+ }
+
+ override fun dec() {
+ lock.withLock {
+ counter--
+ if (counter == 0) {
+ idle.signalAll()
+ }
+ }
+ }
+
+ fun awaitIdle(time: Long, timeUnit: TimeUnit): Boolean {
+ lock.withLock {
+ if (counter == 0) {
+ return true
+ }
+ return idle.await(time, timeUnit)
+ }
+ }
+}
\ No newline at end of file
diff --git a/lifecycle/runtime/eap/src/androidTest/java/androidx/lifecycle/TrackedExecutor.kt b/lifecycle/runtime/eap/src/androidTest/java/androidx/lifecycle/TrackedExecutor.kt
new file mode 100644
index 0000000..d33030c
--- /dev/null
+++ b/lifecycle/runtime/eap/src/androidTest/java/androidx/lifecycle/TrackedExecutor.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2019 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 androidx.lifecycle
+
+import java.util.concurrent.Executor
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.TimeUnit
+
+/**
+ * An executor wrapper that tracks active tasks and reports back to a Callback when it changes.
+ */
+class TrackedExecutor(
+ private val callback: Callback,
+ private val delegate: ExecutorService
+) : Executor {
+ override fun execute(runnable: Runnable) {
+ callback.inc()
+ delegate.execute {
+ try {
+ runnable.run()
+ } finally {
+ callback.dec()
+ }
+ }
+ }
+
+ fun shutdown(time: Long, unit: TimeUnit): Boolean {
+ delegate.shutdown()
+ return delegate.awaitTermination(time, unit)
+ }
+
+ interface Callback {
+ fun inc()
+ fun dec()
+ }
+}
\ No newline at end of file
diff --git a/lifecycle/runtime/eap/src/main/AndroidManifest.xml b/lifecycle/runtime/eap/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..1c2bf90
--- /dev/null
+++ b/lifecycle/runtime/eap/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2019 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.lifecycle.ktx">
+</manifest>
diff --git a/lifecycle/runtime/eap/src/main/java/androidx/lifecycle/DispatchQueue.kt b/lifecycle/runtime/eap/src/main/java/androidx/lifecycle/DispatchQueue.kt
new file mode 100644
index 0000000..87e3d08
--- /dev/null
+++ b/lifecycle/runtime/eap/src/main/java/androidx/lifecycle/DispatchQueue.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2019 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 androidx.lifecycle
+
+import android.annotation.SuppressLint
+import androidx.annotation.AnyThread
+import androidx.annotation.MainThread
+import kotlinx.coroutines.Dispatchers
+import java.util.ArrayDeque
+import java.util.Queue
+import kotlin.coroutines.EmptyCoroutineContext
+
+/**
+ * Helper class for [PausingDispatcher] that tracks runnables which are enqueued to the dispatcher
+ * and also calls back the [PausingDispatcher] when the runnable should run.
+ */
+internal class DispatchQueue {
+ // handler thread
+ private var paused: Boolean = false
+ // handler thread
+ private var finished: Boolean = false
+
+ private val queue: Queue<Runnable> = ArrayDeque<Runnable>()
+
+ private val consumer = Runnable {
+ // this one runs inside Dispatchers.Main
+ // if it should run, grabs an item, runs it
+ // if it has more, will re-enqueue
+ // To avoid starving Dispatchers.Main, we don't consume more than 1
+ if (!canRun()) {
+ return@Runnable
+ }
+ val next = queue.poll() ?: return@Runnable
+ try {
+ next.run()
+ } finally {
+ maybeEnqueueConsumer()
+ }
+ }
+
+ @MainThread
+ fun pause() {
+ paused = true
+ }
+
+ @MainThread
+ fun resume() {
+ if (!paused) {
+ return
+ }
+ check(!finished) {
+ "Cannot resume a finished dispatcher"
+ }
+ paused = false
+ maybeEnqueueConsumer()
+ }
+
+ @MainThread
+ fun finish() {
+ finished = true
+ maybeEnqueueConsumer()
+ }
+
+ @MainThread
+ fun maybeEnqueueConsumer() {
+ if (queue.isNotEmpty()) {
+ Dispatchers.Main.dispatch(EmptyCoroutineContext, consumer)
+ }
+ }
+
+ @MainThread
+ private fun canRun() = finished || !paused
+
+ @AnyThread
+ @SuppressLint("WrongThread") // false negative, we are checking the thread
+ fun runOrEnqueue(runnable: Runnable) {
+ Dispatchers.Main.immediate.dispatch(EmptyCoroutineContext, Runnable {
+ enqueue(runnable)
+ })
+ }
+
+ @MainThread
+ private fun enqueue(runnable: Runnable) {
+ check(queue.offer(runnable)) {
+ "cannot enqueue any more runnables"
+ }
+ maybeEnqueueConsumer()
+ }
+}
diff --git a/lifecycle/runtime/eap/src/main/java/androidx/lifecycle/LifecycleController.kt b/lifecycle/runtime/eap/src/main/java/androidx/lifecycle/LifecycleController.kt
new file mode 100644
index 0000000..4edc2fb
--- /dev/null
+++ b/lifecycle/runtime/eap/src/main/java/androidx/lifecycle/LifecycleController.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2019 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 androidx.lifecycle
+
+import androidx.annotation.MainThread
+import kotlinx.coroutines.Job
+
+/**
+ * Attaches to a lifecycle and controls the [DispatchQueue]'s execution.
+ */
+@MainThread
+internal class LifecycleController(
+ private val lifecycle: Lifecycle,
+ private val minState: Lifecycle.State,
+ private val dispatchQueue: DispatchQueue,
+ parentJob: Job
+) {
+ private val observer = LifecycleEventObserver { source, _ ->
+ if (source.lifecycle.currentState == Lifecycle.State.DESTROYED) {
+ // cancel job before resuming remaining coroutines so that they run in cancelled
+ // state
+ handleDestroy(parentJob)
+ } else if (source.lifecycle.currentState < minState) {
+ dispatchQueue.pause()
+ } else {
+ dispatchQueue.resume()
+ }
+ }
+
+ init {
+ // If Lifecycle is already destroyed (e.g. developer leaked the lifecycle), we won't get
+ // an event callback so we need to check for it before registering
+ // see: b/128749497 for details.
+ if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
+ handleDestroy(parentJob)
+ } else {
+ lifecycle.addObserver(observer)
+ }
+ }
+
+ @Suppress("NOTHING_TO_INLINE") // avoid unnecessary method
+ private inline fun handleDestroy(parentJob: Job) {
+ parentJob.cancel()
+ finish()
+ }
+
+ /**
+ * Removes the observer and also marks the [DispatchQueue] as finished so that any remaining
+ * runnables can be executed.
+ */
+ @MainThread
+ fun finish() {
+ lifecycle.removeObserver(observer)
+ dispatchQueue.finish()
+ }
+}
\ No newline at end of file
diff --git a/lifecycle/runtime/eap/src/main/java/androidx/lifecycle/PausingDispatcher.kt b/lifecycle/runtime/eap/src/main/java/androidx/lifecycle/PausingDispatcher.kt
new file mode 100644
index 0000000..d40e75d
--- /dev/null
+++ b/lifecycle/runtime/eap/src/main/java/androidx/lifecycle/PausingDispatcher.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2019 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 androidx.lifecycle
+
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.Runnable
+import kotlinx.coroutines.withContext
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * Runs the given block when the [LifecycleOwner]'s [Lifecycle] is at least in
+ * [Lifecycle.State.CREATED] state.
+ *
+ * @see Lifecycle.whenStateAtLeast for details
+ */
+suspend fun <T> LifecycleOwner.whenCreated(block: suspend CoroutineScope.() -> T): T =
+ lifecycle.whenCreated(block)
+
+/**
+ * Runs the given block when the [Lifecycle] is at least in [Lifecycle.State.CREATED] state.
+ *
+ * @see Lifecycle.whenStateAtLeast for details
+ */
+suspend fun <T> Lifecycle.whenCreated(block: suspend CoroutineScope.() -> T): T {
+ return whenStateAtLeast(Lifecycle.State.CREATED, block)
+}
+
+/**
+ * Runs the given block when the [LifecycleOwner]'s [Lifecycle] is at least in
+ * [Lifecycle.State.STARTED] state.
+ *
+ * @see Lifecycle.whenStateAtLeast for details
+ */
+suspend fun <T> LifecycleOwner.whenStarted(block: suspend CoroutineScope.() -> T): T =
+ lifecycle.whenStarted(block)
+
+/**
+ * Runs the given block when the [Lifecycle] is at least in [Lifecycle.State.STARTED] state.
+ *
+ * @see Lifecycle.whenStateAtLeast for details
+ */
+suspend fun <T> Lifecycle.whenStarted(block: suspend CoroutineScope.() -> T): T {
+ return whenStateAtLeast(Lifecycle.State.STARTED, block)
+}
+
+/**
+ * Runs the given block when the [LifecycleOwner]'s [Lifecycle] is at least in
+ * [Lifecycle.State.RESUMED] state.
+ *
+ * @see Lifecycle.whenStateAtLeast for details
+ */
+suspend fun <T> LifecycleOwner.whenResumed(block: suspend CoroutineScope.() -> T): T =
+ lifecycle.whenResumed(block)
+
+/**
+ * Runs the given block when the [Lifecycle] is at least in [Lifecycle.State.RESUMED] state.
+ *
+ * @see Lifecycle.whenStateAtLeast for details
+ */
+suspend fun <T> Lifecycle.whenResumed(block: suspend CoroutineScope.() -> T): T {
+ return whenStateAtLeast(Lifecycle.State.RESUMED, block)
+}
+
+/**
+ * Runs the given [block] on a [CoroutineDispatcher] that executes the [block] on the main thread
+ * and suspends the execution unless the [Lifecycle]'s state is at least [minState].
+ *
+ * If the [Lifecycle] moves to a lesser state while the [block] is running, the [block] will
+ * be suspended until the [Lifecycle] reaches to a state greater or equal to [minState].
+ *
+ * Note that this won't effect any sub coroutine if they use a different [CoroutineDispatcher].
+ * However, the [block] will not resume execution when the sub coroutine finishes unless the
+ * [Lifecycle] is at least in [minState].
+ *
+ * If the [Lifecycle] is destroyed while the [block] is suspended, the [block] will be cancelled
+ * which will also cancel any child coroutine launched inside the [block].
+ *
+ * If you have a `try finally` block in your code, the `finally` might run after the [Lifecycle]
+ * moves outside the desired state. It is recommended to check the [Lifecycle.getCurrentState]
+ * before accessing the UI. Similarly, if you have a `catch` statement that might catch
+ * `CancellationException`, you should check the [Lifecycle.getCurrentState] before accessing the
+ * UI. See the sample below for more details.
+ *
+ * ```
+ * // running a block of code only if lifecycle is STARTED
+ * viewLifecycle.whenStateAtLeast(Lifecycle.State.STARTED) {
+ * // here, we are on the main thread and view lifecycle is guaranteed to be STARTED or RESUMED.
+ * // We can safely access our views.
+ * loadingBar.visibility = View.VISIBLE
+ * try {
+ * // we can call any suspend function
+ * val data = withContext(Dispatchers.IO) {
+ * // this will run in IO thread pool. It will keep running as long as Lifecycle
+ * // is not DESTROYED. If it is destroyed, this coroutine will be cancelled as well.
+ * // However, we CANNOT access Views here.
+ *
+ * // We are using withContext(Dispatchers.IO) here just for demonstration purposes.
+ * // Such code should live in your business logic classes and your UI should use a
+ * // ViewModel (or similar) to access it.
+ * api.getUser()
+ * }
+ * // this line will execute on the main thread and only if the lifecycle is in at least
+ * // STARTED state (STARTED is the parameter we've passed to whenStateAtLeast)
+ * // Because of this guarantee, we can safely access the UI again.
+ * loadingBar.visibility = View.GONE
+ * nameTextView.text = user.name
+ * lastNameTextView.text = user.lastName
+ * } catch(ex : UserNotFoundException) {
+ * // same as above, this code can safely access UI elements because it only runs if
+ * // view lifecycle is at least STARTED
+ * loadingBar.visibility = View.GONE
+ * showErrorDialog(ex)
+ * } catch(th : Throwable) {
+ * // Unlike the catch statement above, this catch statements it too generic and might
+ * // also catch the CancellationException. Before accessing UI, you should check isActive
+ * // or lifecycle state
+ * if (viewLifecycle.currentState >= Lifecycle.State.STARTED) {
+ * // here you can access the view because you've checked the coroutine is active
+ * }
+ * } finally {
+ * // in case of cancellation, this line might run even if the Lifecycle is not DESTROYED.
+ * // You cannot access Views here unless you check `isActive` or lifecycle state
+ * if (viewLifecycle.currentState >= Lifecycle.State.STARTED) {
+ * // safe to access views
+ * } else {
+ * // not safe to access views
+ * }
+ * }
+ * }
+ * ```
+ *
+ * @param minState The desired minimum state to run the [block].
+ * @param block The block to run when the lifecycle is at least in [minState].
+ * @return <T> The return value of the [block]
+ */
+suspend fun <T> Lifecycle.whenStateAtLeast(
+ minState: Lifecycle.State,
+ block: suspend CoroutineScope.() -> T
+) = withContext(Dispatchers.Main) {
+ val job = coroutineContext[Job] ?: error("when[State] methods should have a parent job")
+ val dispatcher = PausingDispatcher()
+ val controller =
+ LifecycleController(this@whenStateAtLeast, minState, dispatcher.dispatchQueue, job)
+ try {
+ withContext(dispatcher, block)
+ } finally {
+ controller.finish()
+ }
+}
+
+/**
+ * A [CoroutineDispatcher] implementation that maintains a dispatch queue to be able to pause
+ * execution of coroutines.
+ *
+ * @see [DispatchQueue] and [Lifecycle.whenStateAtLeast] for details.
+ */
+internal class PausingDispatcher : CoroutineDispatcher() {
+ /**
+ * helper class to maintain state and enqueued continuations.
+ */
+ @JvmField
+ internal val dispatchQueue = DispatchQueue()
+
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ dispatchQueue.runOrEnqueue(block)
+ }
+}
\ No newline at end of file
diff --git a/media/build.gradle b/media/build.gradle
index 26e120a..16eb997 100644
--- a/media/build.gradle
+++ b/media/build.gradle
@@ -7,7 +7,7 @@
}
dependencies {
- api(project(":core"))
+ api("androidx.core:core:1.1.0-alpha05")
implementation("androidx.collection:collection:1.0.0")
androidTestImplementation(TEST_EXT_JUNIT)
diff --git a/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java b/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java
index d2b97f8..d9ffcd9 100644
--- a/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java
+++ b/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java
@@ -1103,7 +1103,9 @@
mConnections.remove(b);
ConnectionRecord connection = null;
- for (ConnectionRecord pendingConnection : mPendingConnections) {
+ Iterator<ConnectionRecord> iter = mPendingConnections.iterator();
+ while (iter.hasNext()) {
+ ConnectionRecord pendingConnection = iter.next();
// Note: We cannot use Map/Set for mPendingConnections but List because
// multiple MediaBrowserCompats with the same UID can request connect.
if (pendingConnection.uid == uid) {
@@ -1115,7 +1117,7 @@
pendingConnection.pid, pendingConnection.uid,
rootHints, callbacks);
}
- mPendingConnections.remove(pendingConnection);
+ iter.remove();
}
}
if (connection == null) {
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/MediaBrowserCompatTest.java b/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/MediaBrowserCompatTest.java
index 411f14d..8defe09 100644
--- a/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/MediaBrowserCompatTest.java
+++ b/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/MediaBrowserCompatTest.java
@@ -63,6 +63,7 @@
import static org.junit.Assert.fail;
import android.content.ComponentName;
+import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.support.mediacompat.testlib.util.PollingCheck;
@@ -315,6 +316,54 @@
@Test
@MediumTest
+ public void testMultipleConnections() throws Exception {
+ final Context context = getInstrumentation().getTargetContext();
+ final StubConnectionCallback callback1 = new StubConnectionCallback();
+ final StubConnectionCallback callback2 = new StubConnectionCallback();
+ final StubConnectionCallback callback3 = new StubConnectionCallback();
+ final List<MediaBrowserCompat> browserList = new ArrayList<>();
+
+ getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ MediaBrowserCompat browser1 = new MediaBrowserCompat(context, TEST_BROWSER_SERVICE,
+ callback1, new Bundle());
+ MediaBrowserCompat browser2 = new MediaBrowserCompat(context, TEST_BROWSER_SERVICE,
+ callback2, new Bundle());
+ MediaBrowserCompat browser3 = new MediaBrowserCompat(context, TEST_BROWSER_SERVICE,
+ callback3, new Bundle());
+
+ browserList.add(browser1);
+ browserList.add(browser2);
+ browserList.add(browser3);
+
+ browser1.connect();
+ browser2.connect();
+ browser3.connect();
+ }
+ });
+
+ try {
+ new PollingCheck(TIME_OUT_MS) {
+ @Override
+ protected boolean check() {
+ return callback1.mConnectedCount == 1
+ && callback2.mConnectedCount == 1
+ && callback3.mConnectedCount == 1;
+ }
+ }.run();
+ } finally {
+ for (int i = 0; i < browserList.size(); i++) {
+ MediaBrowserCompat browser = browserList.get(i);
+ if (browser.isConnected()) {
+ browser.disconnect();
+ }
+ }
+ }
+ }
+
+ @Test
+ @MediumTest
public void testSubscribe() throws Exception {
connectMediaBrowserService();
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/MediaBrowserTest.java b/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/MediaBrowserTest.java
index fe57150..185f0f5 100644
--- a/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/MediaBrowserTest.java
+++ b/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/MediaBrowserTest.java
@@ -43,6 +43,7 @@
import static org.junit.Assert.fail;
import android.content.ComponentName;
+import android.content.Context;
import android.media.MediaDescription;
import android.media.browse.MediaBrowser;
import android.media.browse.MediaBrowser.MediaItem;
@@ -295,6 +296,54 @@
@Test
@MediumTest
+ public void testMultipleConnections() throws Exception {
+ final Context context = getInstrumentation().getTargetContext();
+ final StubConnectionCallback callback1 = new StubConnectionCallback();
+ final StubConnectionCallback callback2 = new StubConnectionCallback();
+ final StubConnectionCallback callback3 = new StubConnectionCallback();
+ final List<MediaBrowser> browserList = new ArrayList<>();
+
+ getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ MediaBrowser browser1 = new MediaBrowser(context, TEST_BROWSER_SERVICE,
+ callback1, new Bundle());
+ MediaBrowser browser2 = new MediaBrowser(context, TEST_BROWSER_SERVICE,
+ callback2, new Bundle());
+ MediaBrowser browser3 = new MediaBrowser(context, TEST_BROWSER_SERVICE,
+ callback3, new Bundle());
+
+ browserList.add(browser1);
+ browserList.add(browser2);
+ browserList.add(browser3);
+
+ browser1.connect();
+ browser2.connect();
+ browser3.connect();
+ }
+ });
+
+ try {
+ new PollingCheck(TIME_OUT_MS) {
+ @Override
+ protected boolean check() {
+ return callback1.mConnectedCount == 1
+ && callback2.mConnectedCount == 1
+ && callback3.mConnectedCount == 1;
+ }
+ }.run();
+ } finally {
+ for (int i = 0; i < browserList.size(); i++) {
+ MediaBrowser browser = browserList.get(i);
+ if (browser.isConnected()) {
+ browser.disconnect();
+ }
+ }
+ }
+ }
+
+ @Test
+ @MediumTest
public void testSubscribe() throws Exception {
connectMediaBrowserService();
diff --git a/media/version-compat-tests/previous/client/src/androidTest/java/android/support/mediacompat/client/MediaBrowserCompatTest.java b/media/version-compat-tests/previous/client/src/androidTest/java/android/support/mediacompat/client/MediaBrowserCompatTest.java
index ffabc62..7a7d0c1 100644
--- a/media/version-compat-tests/previous/client/src/androidTest/java/android/support/mediacompat/client/MediaBrowserCompatTest.java
+++ b/media/version-compat-tests/previous/client/src/androidTest/java/android/support/mediacompat/client/MediaBrowserCompatTest.java
@@ -59,6 +59,7 @@
import static org.junit.Assert.fail;
import android.content.ComponentName;
+import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
@@ -309,6 +310,54 @@
@Test
@MediumTest
+ public void testMultipleConnections() throws Exception {
+ final Context context = getInstrumentation().getTargetContext();
+ final StubConnectionCallback callback1 = new StubConnectionCallback();
+ final StubConnectionCallback callback2 = new StubConnectionCallback();
+ final StubConnectionCallback callback3 = new StubConnectionCallback();
+ final List<MediaBrowserCompat> browserList = new ArrayList<>();
+
+ getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ MediaBrowserCompat browser1 = new MediaBrowserCompat(context, TEST_BROWSER_SERVICE,
+ callback1, new Bundle());
+ MediaBrowserCompat browser2 = new MediaBrowserCompat(context, TEST_BROWSER_SERVICE,
+ callback2, new Bundle());
+ MediaBrowserCompat browser3 = new MediaBrowserCompat(context, TEST_BROWSER_SERVICE,
+ callback3, new Bundle());
+
+ browserList.add(browser1);
+ browserList.add(browser2);
+ browserList.add(browser3);
+
+ browser1.connect();
+ browser2.connect();
+ browser3.connect();
+ }
+ });
+
+ try {
+ new PollingCheck(TIME_OUT_MS) {
+ @Override
+ protected boolean check() {
+ return callback1.mConnectedCount == 1
+ && callback2.mConnectedCount == 1
+ && callback3.mConnectedCount == 1;
+ }
+ }.run();
+ } finally {
+ for (int i = 0; i < browserList.size(); i++) {
+ MediaBrowserCompat browser = browserList.get(i);
+ if (browser.isConnected()) {
+ browser.disconnect();
+ }
+ }
+ }
+ }
+
+ @Test
+ @MediumTest
public void testSubscribe() throws Exception {
connectMediaBrowserService();
diff --git a/media2-widget/src/androidTest/java/androidx/media2/widget/MediaControlViewTest.java b/media2-widget/src/androidTest/java/androidx/media2/widget/MediaControlViewTest.java
index e6acd7e..c381616 100644
--- a/media2-widget/src/androidTest/java/androidx/media2/widget/MediaControlViewTest.java
+++ b/media2-widget/src/androidTest/java/androidx/media2/widget/MediaControlViewTest.java
@@ -462,7 +462,7 @@
}
private MediaItem createTestMediaItem2(Uri uri) {
- return new UriMediaItem.Builder(mVideoView.getContext(), uri).build();
+ return new UriMediaItem.Builder(uri).build();
}
private MediaController createController(MediaController.ControllerCallback callback) {
diff --git a/media2-widget/src/androidTest/java/androidx/media2/widget/VideoViewTest.java b/media2-widget/src/androidTest/java/androidx/media2/widget/VideoViewTest.java
index 5d4bf45..4f01d40 100644
--- a/media2-widget/src/androidTest/java/androidx/media2/widget/VideoViewTest.java
+++ b/media2-widget/src/androidTest/java/androidx/media2/widget/VideoViewTest.java
@@ -398,7 +398,6 @@
Uri testVideoUri = Uri.parse(
"android.resource://" + mContext.getPackageName() + "/"
+ R.raw.testvideo_with_2_subtitle_tracks);
- return new UriMediaItem.Builder(mVideoView.getContext(), testVideoUri)
- .build();
+ return new UriMediaItem.Builder(testVideoUri).build();
}
}
diff --git a/media2-widget/src/main/java/androidx/media2/widget/VideoView.java b/media2-widget/src/main/java/androidx/media2/widget/VideoView.java
index cf1ba6f..a619381 100644
--- a/media2-widget/src/main/java/androidx/media2/widget/VideoView.java
+++ b/media2-widget/src/main/java/androidx/media2/widget/VideoView.java
@@ -296,9 +296,6 @@
if (DEBUG) {
Log.d(TAG, "onSurfaceTakeOverDone(). Now current view is: " + view);
}
- if (mCurrentState != STATE_PLAYING && mMediaSession != null) {
- mMediaSession.getPlayer().seekTo(mMediaSession.getPlayer().getCurrentPosition());
- }
if (view != mCurrentView) {
((View) mCurrentView).setVisibility(View.GONE);
mCurrentView = view;
@@ -467,7 +464,14 @@
/**
* Selects which view will be used to render video between SurfaceView and TextureView.
- *
+ * <p>
+ * Note: There are two known issues on API level 28+ devices.
+ * <ul>
+ * <li> When changing view type to SurfaceView from TextureView in "paused" playback state,
+ * a blank screen can be shown.
+ * <li> When changing view type to TextureView from SurfaceView repeatedly in "paused" playback
+ * state, the lastly rendered frame on TextureView can be shown.
+ * </ul>
* @param viewType the view type to render video
* <ul>
* <li>{@link #VIEW_TYPE_SURFACEVIEW}
@@ -773,9 +777,7 @@
SubtitleTrack track = mSubtitleController.addTrack(trackInfos.get(i).getFormat());
if (track != null) {
mSubtitleTracks.put(i, track);
- String language =
- (trackInfos.get(i).getLanguage().equals(SUBTITLE_TRACK_LANG_UNDEFINED))
- ? "" : trackInfos.get(i).getLanguage();
+ String language = trackInfos.get(i).getLanguage().getISO3Language();
subtitleTracksLanguageList.add(language);
}
}
@@ -784,6 +786,10 @@
if (mAudioTrackIndices.size() > 0) {
mSelectedAudioTrackIndex = 0;
}
+ // Re-select originally selected subtitle track since SubtitleController has been reset.
+ if (mSelectedSubtitleTrackIndex != INVALID_TRACK_INDEX) {
+ selectSubtitleTrack(mSelectedSubtitleTrackIndex);
+ }
Bundle data = new Bundle();
data.putInt(MediaControlView.KEY_VIDEO_TRACK_COUNT, mVideoTrackIndices.size());
diff --git a/media2/api/1.0.0-alpha05.txt b/media2/api/1.0.0-alpha05.txt
index ba118d1..d753947 100644
--- a/media2/api/1.0.0-alpha05.txt
+++ b/media2/api/1.0.0-alpha05.txt
@@ -316,7 +316,7 @@
method public float getMaxPlayerVolume();
method public int getNextMediaItemIndex();
method public androidx.media2.PlaybackParams getPlaybackParams();
- method public float getPlaybackSpeed();
+ method @FloatRange(from=0.0f, to=java.lang.Float.MAX_VALUE, fromInclusive=false) public float getPlaybackSpeed();
method public int getPlayerState();
method public float getPlayerVolume();
method public java.util.List<androidx.media2.MediaItem>? getPlaylist();
@@ -331,6 +331,7 @@
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> pause();
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> play();
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> prepare();
+ method public void registerPlayerCallback(java.util.concurrent.Executor, androidx.media2.MediaPlayer.PlayerCallback);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> removePlaylistItem(@IntRange(from=0) int);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> replacePlaylistItem(int, androidx.media2.MediaItem);
method public void reset();
@@ -342,7 +343,7 @@
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setAuxEffectSendLevel(@FloatRange(from=0, to=1) float);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setMediaItem(androidx.media2.MediaItem);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setPlaybackParams(androidx.media2.PlaybackParams);
- method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setPlaybackSpeed(@FloatRange(from=0, to=1) float);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setPlaybackSpeed(@FloatRange(from=0.0f, to=java.lang.Float.MAX_VALUE, fromInclusive=false) float);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setPlayerVolume(@FloatRange(from=0, to=1) float);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setPlaylist(java.util.List<androidx.media2.MediaItem>, androidx.media2.MediaMetadata?);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setRepeatMode(int);
@@ -351,6 +352,7 @@
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> skipToNextPlaylistItem();
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> skipToPlaylistItem(@IntRange(from=0) int);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> skipToPreviousPlaylistItem();
+ method public void unregisterPlayerCallback(androidx.media2.MediaPlayer.PlayerCallback);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> updatePlaylistMetadata(androidx.media2.MediaMetadata?);
field public static final int MEDIA_INFO_AUDIO_NOT_PLAYING = 804; // 0x324
field public static final int MEDIA_INFO_BAD_INTERLEAVING = 800; // 0x320
@@ -360,6 +362,7 @@
field public static final int MEDIA_INFO_VIDEO_NOT_PLAYING = 805; // 0x325
field public static final int MEDIA_INFO_VIDEO_RENDERING_START = 3; // 0x3
field public static final int MEDIA_INFO_VIDEO_TRACK_LAGGING = 700; // 0x2bc
+ field public static final int NO_TRACK_SELECTED = -2147483648; // 0x80000000
field public static final int PLAYER_ERROR_IO = -1004; // 0xfffffc14
field public static final int PLAYER_ERROR_MALFORMED = -1007; // 0xfffffc11
field public static final int PLAYER_ERROR_TIMED_OUT = -110; // 0xffffff92
@@ -383,7 +386,7 @@
public static final class MediaPlayer.TrackInfo {
method public android.media.MediaFormat? getFormat();
- method public String getLanguage();
+ method public java.util.Locale getLanguage();
method public int getTrackType();
field public static final int MEDIA_TRACK_TYPE_AUDIO = 2; // 0x2
field public static final int MEDIA_TRACK_TYPE_METADATA = 5; // 0x5
@@ -501,8 +504,8 @@
ctor public PlaybackParams.Builder(androidx.media2.PlaybackParams);
method public androidx.media2.PlaybackParams build();
method public androidx.media2.PlaybackParams.Builder setAudioFallbackMode(int);
- method public androidx.media2.PlaybackParams.Builder setPitch(float);
- method public androidx.media2.PlaybackParams.Builder setSpeed(float);
+ method public androidx.media2.PlaybackParams.Builder setPitch(@FloatRange(from=0.0f, to=java.lang.Float.MAX_VALUE) float);
+ method public androidx.media2.PlaybackParams.Builder setSpeed(@FloatRange(from=0.0f, to=java.lang.Float.MAX_VALUE, fromInclusive=false) float);
}
public interface Rating extends androidx.versionedparcelable.VersionedParcelable {
@@ -724,14 +727,13 @@
public class UriMediaItem extends androidx.media2.MediaItem {
method public android.net.Uri getUri();
- method public android.content.Context getUriContext();
method public java.util.List<java.net.HttpCookie>? getUriCookies();
method public java.util.Map<java.lang.String,java.lang.String>? getUriHeaders();
}
public static final class UriMediaItem.Builder extends androidx.media2.MediaItem.Builder {
- ctor public UriMediaItem.Builder(android.content.Context, android.net.Uri);
- ctor public UriMediaItem.Builder(android.content.Context, android.net.Uri, java.util.Map<java.lang.String,java.lang.String>?, java.util.List<java.net.HttpCookie>?);
+ ctor public UriMediaItem.Builder(android.net.Uri);
+ ctor public UriMediaItem.Builder(android.net.Uri, java.util.Map<java.lang.String,java.lang.String>?, java.util.List<java.net.HttpCookie>?);
method public androidx.media2.UriMediaItem build();
method public androidx.media2.UriMediaItem.Builder setEndPosition(long);
method public androidx.media2.UriMediaItem.Builder setMetadata(androidx.media2.MediaMetadata?);
diff --git a/media2/api/current.txt b/media2/api/current.txt
index ba118d1..d753947 100644
--- a/media2/api/current.txt
+++ b/media2/api/current.txt
@@ -316,7 +316,7 @@
method public float getMaxPlayerVolume();
method public int getNextMediaItemIndex();
method public androidx.media2.PlaybackParams getPlaybackParams();
- method public float getPlaybackSpeed();
+ method @FloatRange(from=0.0f, to=java.lang.Float.MAX_VALUE, fromInclusive=false) public float getPlaybackSpeed();
method public int getPlayerState();
method public float getPlayerVolume();
method public java.util.List<androidx.media2.MediaItem>? getPlaylist();
@@ -331,6 +331,7 @@
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> pause();
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> play();
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> prepare();
+ method public void registerPlayerCallback(java.util.concurrent.Executor, androidx.media2.MediaPlayer.PlayerCallback);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> removePlaylistItem(@IntRange(from=0) int);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> replacePlaylistItem(int, androidx.media2.MediaItem);
method public void reset();
@@ -342,7 +343,7 @@
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setAuxEffectSendLevel(@FloatRange(from=0, to=1) float);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setMediaItem(androidx.media2.MediaItem);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setPlaybackParams(androidx.media2.PlaybackParams);
- method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setPlaybackSpeed(@FloatRange(from=0, to=1) float);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setPlaybackSpeed(@FloatRange(from=0.0f, to=java.lang.Float.MAX_VALUE, fromInclusive=false) float);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setPlayerVolume(@FloatRange(from=0, to=1) float);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setPlaylist(java.util.List<androidx.media2.MediaItem>, androidx.media2.MediaMetadata?);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setRepeatMode(int);
@@ -351,6 +352,7 @@
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> skipToNextPlaylistItem();
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> skipToPlaylistItem(@IntRange(from=0) int);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> skipToPreviousPlaylistItem();
+ method public void unregisterPlayerCallback(androidx.media2.MediaPlayer.PlayerCallback);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> updatePlaylistMetadata(androidx.media2.MediaMetadata?);
field public static final int MEDIA_INFO_AUDIO_NOT_PLAYING = 804; // 0x324
field public static final int MEDIA_INFO_BAD_INTERLEAVING = 800; // 0x320
@@ -360,6 +362,7 @@
field public static final int MEDIA_INFO_VIDEO_NOT_PLAYING = 805; // 0x325
field public static final int MEDIA_INFO_VIDEO_RENDERING_START = 3; // 0x3
field public static final int MEDIA_INFO_VIDEO_TRACK_LAGGING = 700; // 0x2bc
+ field public static final int NO_TRACK_SELECTED = -2147483648; // 0x80000000
field public static final int PLAYER_ERROR_IO = -1004; // 0xfffffc14
field public static final int PLAYER_ERROR_MALFORMED = -1007; // 0xfffffc11
field public static final int PLAYER_ERROR_TIMED_OUT = -110; // 0xffffff92
@@ -383,7 +386,7 @@
public static final class MediaPlayer.TrackInfo {
method public android.media.MediaFormat? getFormat();
- method public String getLanguage();
+ method public java.util.Locale getLanguage();
method public int getTrackType();
field public static final int MEDIA_TRACK_TYPE_AUDIO = 2; // 0x2
field public static final int MEDIA_TRACK_TYPE_METADATA = 5; // 0x5
@@ -501,8 +504,8 @@
ctor public PlaybackParams.Builder(androidx.media2.PlaybackParams);
method public androidx.media2.PlaybackParams build();
method public androidx.media2.PlaybackParams.Builder setAudioFallbackMode(int);
- method public androidx.media2.PlaybackParams.Builder setPitch(float);
- method public androidx.media2.PlaybackParams.Builder setSpeed(float);
+ method public androidx.media2.PlaybackParams.Builder setPitch(@FloatRange(from=0.0f, to=java.lang.Float.MAX_VALUE) float);
+ method public androidx.media2.PlaybackParams.Builder setSpeed(@FloatRange(from=0.0f, to=java.lang.Float.MAX_VALUE, fromInclusive=false) float);
}
public interface Rating extends androidx.versionedparcelable.VersionedParcelable {
@@ -724,14 +727,13 @@
public class UriMediaItem extends androidx.media2.MediaItem {
method public android.net.Uri getUri();
- method public android.content.Context getUriContext();
method public java.util.List<java.net.HttpCookie>? getUriCookies();
method public java.util.Map<java.lang.String,java.lang.String>? getUriHeaders();
}
public static final class UriMediaItem.Builder extends androidx.media2.MediaItem.Builder {
- ctor public UriMediaItem.Builder(android.content.Context, android.net.Uri);
- ctor public UriMediaItem.Builder(android.content.Context, android.net.Uri, java.util.Map<java.lang.String,java.lang.String>?, java.util.List<java.net.HttpCookie>?);
+ ctor public UriMediaItem.Builder(android.net.Uri);
+ ctor public UriMediaItem.Builder(android.net.Uri, java.util.Map<java.lang.String,java.lang.String>?, java.util.List<java.net.HttpCookie>?);
method public androidx.media2.UriMediaItem build();
method public androidx.media2.UriMediaItem.Builder setEndPosition(long);
method public androidx.media2.UriMediaItem.Builder setMetadata(androidx.media2.MediaMetadata?);
diff --git a/media2/src/androidTest/java/androidx/media2/MediaPlayer2DrmTestBase.java b/media2/src/androidTest/java/androidx/media2/MediaPlayer2DrmTestBase.java
index cd59aca..15eb2ba 100644
--- a/media2/src/androidTest/java/androidx/media2/MediaPlayer2DrmTestBase.java
+++ b/media2/src/androidTest/java/androidx/media2/MediaPlayer2DrmTestBase.java
@@ -302,8 +302,7 @@
mPlayer.setEventCallback(mExecutor, mECb);
Log.v(TAG, "playLoadedVideo: setMediaItem()");
- mPlayer.setMediaItem(
- new UriMediaItem.Builder(mContext, file).build());
+ mPlayer.setMediaItem(new UriMediaItem.Builder(file).build());
mSetDataSourceCallCompleted.waitForSignal();
if (mCallStatus != MediaPlayer2.CALL_STATUS_NO_ERROR) {
throw new PrepareFailedException();
@@ -557,8 +556,7 @@
mPlayer.setEventCallback(mExecutor, mECb);
Log.v(TAG, "playLoadedVideo: setMediaItem()");
- mPlayer.setMediaItem(
- new UriMediaItem.Builder(mContext, file).build());
+ mPlayer.setMediaItem(new UriMediaItem.Builder(file).build());
Log.v(TAG, "playLoadedVideo: prepare()");
mPlayer.prepare();
diff --git a/media2/src/androidTest/java/androidx/media2/MediaPlayer2Test.java b/media2/src/androidTest/java/androidx/media2/MediaPlayer2Test.java
index 1ac7d97..d7827d1 100644
--- a/media2/src/androidTest/java/androidx/media2/MediaPlayer2Test.java
+++ b/media2/src/androidTest/java/androidx/media2/MediaPlayer2Test.java
@@ -219,7 +219,7 @@
// test stop and restart
mp.reset();
mp.setEventCallback(mExecutor, ecb);
- mp.setMediaItem(new UriMediaItem.Builder(mContext, uri).build());
+ mp.setMediaItem(new UriMediaItem.Builder(uri).build());
onPrepareCalled.reset();
mp.prepare();
onPrepareCalled.waitForSignal();
@@ -2335,7 +2335,7 @@
Uri uri = Uri.parse(outputFileLocation);
MediaPlayer2 mp = MediaPlayer2.create(mActivity);
try {
- mp.setMediaItem(new UriMediaItem.Builder(mContext, uri).build());
+ mp.setMediaItem(new UriMediaItem.Builder(uri).build());
mp.prepare();
Thread.sleep(SLEEP_TIME);
playAndStop(mp);
diff --git a/media2/src/androidTest/java/androidx/media2/MediaPlayer2TestBase.java b/media2/src/androidTest/java/androidx/media2/MediaPlayer2TestBase.java
index c473620..13dd115 100644
--- a/media2/src/androidTest/java/androidx/media2/MediaPlayer2TestBase.java
+++ b/media2/src/androidTest/java/androidx/media2/MediaPlayer2TestBase.java
@@ -121,7 +121,7 @@
new AudioAttributesCompat.Builder().build();
mp.setAudioAttributes(aa);
mp.setAudioSessionId(audioSessionId);
- mp.setMediaItem(new UriMediaItem.Builder(context, uri).build());
+ mp.setMediaItem(new UriMediaItem.Builder(uri).build());
if (holder != null) {
mp.setSurface(holder.getSurface());
}
@@ -417,7 +417,7 @@
final Uri uri = Uri.parse(path);
for (int i = 0; i < STREAM_RETRIES; i++) {
try {
- mPlayer.setMediaItem(new UriMediaItem.Builder(mContext, uri).build());
+ mPlayer.setMediaItem(new UriMediaItem.Builder(uri).build());
playLoadedVideo(width, height, playTime);
playedSuccessfully = true;
break;
@@ -449,8 +449,7 @@
boolean playedSuccessfully = false;
for (int i = 0; i < STREAM_RETRIES; i++) {
try {
- mPlayer.setMediaItem(new UriMediaItem.Builder(
- mContext, uri, headers, cookies).build());
+ mPlayer.setMediaItem(new UriMediaItem.Builder(uri, headers, cookies).build());
playLoadedVideo(width, height, playTime);
playedSuccessfully = true;
break;
diff --git a/media2/src/androidTest/java/androidx/media2/MediaPlayerDrmTest.java b/media2/src/androidTest/java/androidx/media2/MediaPlayerDrmTest.java
index 0195c56..ca8caec 100644
--- a/media2/src/androidTest/java/androidx/media2/MediaPlayerDrmTest.java
+++ b/media2/src/androidTest/java/androidx/media2/MediaPlayerDrmTest.java
@@ -402,7 +402,7 @@
mPlayer.registerPlayerCallback(mExecutor, mECb);
Log.v(TAG, "playLoadedVideo: setMediaItem()");
ListenableFuture<PlayerResult> future =
- mPlayer.setMediaItem(new UriMediaItem.Builder(mContext, file).build());
+ mPlayer.setMediaItem(new UriMediaItem.Builder(file).build());
assertEquals(PlayerResult.RESULT_SUCCESS, future.get().getResultCode());
SurfaceHolder surfaceHolder = mActivity.getSurfaceHolder();
@@ -645,8 +645,7 @@
mPlayer.registerPlayerCallback(mExecutor, mECb);
Log.v(TAG, "playLoadedVideo: setMediaItem()");
- mPlayer.setMediaItem(
- new UriMediaItem.Builder(mContext, file).build());
+ mPlayer.setMediaItem(new UriMediaItem.Builder(file).build());
Log.v(TAG, "playLoadedVideo: prepare()");
ListenableFuture<PlayerResult> future = mPlayer.prepare();
diff --git a/media2/src/androidTest/java/androidx/media2/MediaPlayerTestBase.java b/media2/src/androidTest/java/androidx/media2/MediaPlayerTestBase.java
index 50cbb19..9a19cb9 100644
--- a/media2/src/androidTest/java/androidx/media2/MediaPlayerTestBase.java
+++ b/media2/src/androidTest/java/androidx/media2/MediaPlayerTestBase.java
@@ -117,7 +117,7 @@
.build();
return mPlayer.setMediaItem(new UriMediaItem.Builder(
- mContext, testVideoUri).build()).get().getResultCode()
+ testVideoUri).build()).get().getResultCode()
== androidx.media2.SessionPlayer.PlayerResult.RESULT_SUCCESS;
}
diff --git a/media2/src/main/java/androidx/media2/MediaPlayer.java b/media2/src/main/java/androidx/media2/MediaPlayer.java
index f1d2383..50add35 100644
--- a/media2/src/main/java/androidx/media2/MediaPlayer.java
+++ b/media2/src/main/java/androidx/media2/MediaPlayer.java
@@ -62,6 +62,7 @@
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
@@ -127,6 +128,10 @@
* <td>This is to handle error</td></tr>
* </table>
* <p>
+ * If an {@link AudioAttributesCompat} is not specified by {@link #setAudioAttributes},
+ * {@link #getAudioAttributes} will return {@code null} and the default audio focus behavior will
+ * follow the {@code null} case on the table above.
+ * <p>
* For more information about the audio focus, take a look at
* <a href="{@docRoot}guide/topics/media-apps/audio-focus.html">Managing audio focus</a>
* <p>
@@ -426,6 +431,13 @@
@RestrictTo(LIBRARY_GROUP_PREFIX)
public @interface SeekMode {}
+ /**
+ * The return value of {@link #getSelectedTrack} when there is no selected track for the given
+ * type.
+ * @see #getSelectedTrack(int)
+ */
+ public static final int NO_TRACK_SELECTED = Integer.MIN_VALUE;
+
private static final int CALL_COMPLETE_PLAYLIST_BASE = -1000;
private static final int END_OF_PLAYLIST = -1;
private static final int NO_MEDIA_ITEM = -2;
@@ -805,7 +817,8 @@
@Override
@NonNull
public ListenableFuture<PlayerResult> setPlaybackSpeed(
- @FloatRange(from = 0, to = 1) final float playbackSpeed) {
+ @FloatRange(from = 0.0f, to = Float.MAX_VALUE, fromInclusive = false)
+ final float playbackSpeed) {
PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(mExecutor) {
@Override
List<ResolvableFuture<PlayerResult>> onExecute() {
@@ -849,7 +862,8 @@
}
@Override
- public @PlayerState int getPlayerState() {
+ @PlayerState
+ public int getPlayerState() {
synchronized (mStateLock) {
return mState;
}
@@ -895,7 +909,8 @@
}
@Override
- public @BuffState int getBufferingState() {
+ @BuffState
+ public int getBufferingState() {
Integer buffState;
synchronized (mStateLock) {
buffState = mMediaItemToBuffState.get(mPlayer.getCurrentMediaItem());
@@ -904,6 +919,7 @@
}
@Override
+ @FloatRange(from = 0.0f, to = Float.MAX_VALUE, fromInclusive = false)
public float getPlaybackSpeed() {
try {
return mPlayer.getPlaybackParams().getSpeed();
@@ -1611,7 +1627,8 @@
* receive a notification {@link PlayerCallback#onVideoSizeChanged} when the size
* is available.
*/
- public @NonNull VideoSize getVideoSize() {
+ @NonNull
+ public VideoSize getVideoSize() {
return new VideoSize(mPlayer.getVideoWidth(), mPlayer.getVideoHeight());
}
@@ -1630,13 +1647,7 @@
}
/**
- * Sets playback rate using {@link PlaybackParams}.
- * <p>
- * The player sets its internal PlaybackParams to the given input. This does not change the
- * player state. For example, if this is called with the speed of 2.0f in
- * {@link #PLAYER_STATE_PAUSED}, the player will just update internal property and stay paused.
- * Once the client calls {@link #play()} afterwards, the player will start playback with the
- * given speed. Calling this with zero speed is not allowed.
+ * Sets playback params using {@link PlaybackParams}.
*
* @param params the playback params.
* @return a {@link ListenableFuture} which represents the pending completion of the command.
@@ -1887,8 +1898,9 @@
* @see #selectTrack(int)
* @see #deselectTrack(int)
*/
- public int getSelectedTrack(int trackType) {
- return mPlayer.getSelectedTrack(trackType);
+ public int getSelectedTrack(@TrackInfo.MediaTrackType int trackType) {
+ final int ret = mPlayer.getSelectedTrack(trackType);
+ return (ret < 0) ? NO_TRACK_SELECTED : ret;
}
/**
@@ -1974,6 +1986,29 @@
}
/**
+ * Register {@link PlayerCallback} to listen changes.
+ *
+ * @param executor a callback Executor
+ * @param callback a PlayerCallback
+ * @throws IllegalArgumentException if executor or callback is {@code null}.
+ */
+ public void registerPlayerCallback(
+ @NonNull /*@CallbackExecutor*/ Executor executor,
+ @NonNull PlayerCallback callback) {
+ super.registerPlayerCallback(executor, callback);
+ }
+
+ /**
+ * Unregister the previously registered {@link PlayerCallback}.
+ *
+ * @param callback the callback to be removed
+ * @throws IllegalArgumentException if the callback is {@code null}.
+ */
+ public void unregisterPlayerCallback(@NonNull PlayerCallback callback) {
+ super.unregisterPlayerCallback(callback);
+ }
+
+ /**
* Retrieves the DRM Info associated with the current media item.
*
* @throws IllegalStateException if called before being prepared
@@ -2793,6 +2828,20 @@
public static final int MEDIA_TRACK_TYPE_SUBTITLE = 4;
public static final int MEDIA_TRACK_TYPE_METADATA = 5;
+ /**
+ * @hide
+ */
+ @IntDef(flag = false, /*prefix = "PLAYER_ERROR",*/ value = {
+ MEDIA_TRACK_TYPE_UNKNOWN,
+ MEDIA_TRACK_TYPE_VIDEO,
+ MEDIA_TRACK_TYPE_AUDIO,
+ MEDIA_TRACK_TYPE_SUBTITLE,
+ MEDIA_TRACK_TYPE_METADATA,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ public @interface MediaTrackType {}
+
private final int mTrackType;
private final MediaFormat mFormat;
@@ -2800,20 +2849,21 @@
* Gets the track type.
* @return TrackType which indicates if the track is video, audio, timed text.
*/
- public int getTrackType() {
+ public @MediaTrackType int getTrackType() {
return mTrackType;
}
/**
* Gets the language code of the track.
- * @return a language code in either way of ISO-639-1 or ISO-639-2.
- * When the language is unknown or could not be determined,
- * ISO-639-2 language code, "und", is returned.
+ * @return {@link Locale} which includes the language information.
*/
@NonNull
- public String getLanguage() {
- String language = mFormat.getString(MediaFormat.KEY_LANGUAGE);
- return language == null ? "und" : language;
+ public Locale getLanguage() {
+ String language = mFormat != null ? mFormat.getString(MediaFormat.KEY_LANGUAGE) : null;
+ if (language == null) {
+ language = "und";
+ }
+ return new Locale(language);
}
/**
diff --git a/media2/src/main/java/androidx/media2/MediaPlayer2.java b/media2/src/main/java/androidx/media2/MediaPlayer2.java
index 243d7d1..690d3c1 100644
--- a/media2/src/main/java/androidx/media2/MediaPlayer2.java
+++ b/media2/src/main/java/androidx/media2/MediaPlayer2.java
@@ -243,7 +243,7 @@
if (Build.VERSION.SDK_INT <= 27 || DEBUG_USE_EXOPLAYER) {
return new ExoPlayerMediaPlayer2Impl(context);
} else {
- return new MediaPlayer2Impl();
+ return new MediaPlayer2Impl(context);
}
}
diff --git a/media2/src/main/java/androidx/media2/MediaPlayer2Impl.java b/media2/src/main/java/androidx/media2/MediaPlayer2Impl.java
index 8ead1fe..7816fe1 100644
--- a/media2/src/main/java/androidx/media2/MediaPlayer2Impl.java
+++ b/media2/src/main/java/androidx/media2/MediaPlayer2Impl.java
@@ -17,6 +17,7 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+import android.content.Context;
import android.media.AudioAttributes;
import android.media.DeniedByServerException;
import android.media.MediaDataSource;
@@ -138,6 +139,7 @@
MediaPlayerSourceQueue mPlayer;
private HandlerThread mHandlerThread;
+ private final Context mContext;
private final Handler mEndPositionHandler;
private final Handler mTaskHandler;
@SuppressWarnings("WeakerAccess") /* synthetic access */
@@ -174,7 +176,8 @@
* to free the resources. If not released, too many MediaPlayer2Impl instances may
* result in an exception.</p>
*/
- public MediaPlayer2Impl() {
+ public MediaPlayer2Impl(Context context) {
+ mContext = context;
mHandlerThread = new HandlerThread("MediaPlayer2TaskThread");
mHandlerThread.start();
Looper looper = mHandlerThread.getLooper();
@@ -451,7 +454,7 @@
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
- static void handleDataSource(MediaPlayerSource src)
+ void handleDataSource(MediaPlayerSource src)
throws IOException {
final MediaItem item = src.getDSD();
Preconditions.checkArgument(item != null, "the MediaItem cannot be null");
@@ -488,7 +491,7 @@
} else if (item instanceof UriMediaItem) {
UriMediaItem uitem = (UriMediaItem) item;
player.setDataSource(
- uitem.getUriContext(),
+ mContext,
uitem.getUri(),
uitem.getUriHeaders(),
uitem.getUriCookies());
diff --git a/media2/src/main/java/androidx/media2/PlaybackParams.java b/media2/src/main/java/androidx/media2/PlaybackParams.java
index 2b93cd6..b3d4380 100644
--- a/media2/src/main/java/androidx/media2/PlaybackParams.java
+++ b/media2/src/main/java/androidx/media2/PlaybackParams.java
@@ -21,6 +21,7 @@
import android.media.AudioTrack;
import android.os.Build;
+import androidx.annotation.FloatRange;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -36,8 +37,19 @@
* Used by {@link MediaPlayer} {@link MediaPlayer#getPlaybackParams()} and
* {@link MediaPlayer#setPlaybackParams(PlaybackParams)}
* to control playback behavior.
- * <p> <strong>audio fallback mode:</strong>
- * select out-of-range parameter handling.
+ * <p>
+ * PlaybackParams returned by {@link MediaPlayer#getPlaybackParams()} will always have values.
+ * In case of {@link MediaPlayer#setPlaybackParams}, the player will not update the param if the
+ * value is not set. For example, if pitch is set while speed is not set, only pitch will be
+ * updated.
+ * <p>
+ * Note that the speed value does not change the player state. For example, if
+ * {@link MediaPlayer#getPlaybackParams()} is called with the speed of 2.0f in
+ * {@link MediaPlayer#PLAYER_STATE_PAUSED}, the player will just update internal property and stay
+ * paused. Once {@link MediaPlayer#play()} is called afterwards, the player will start
+ * playback with the given speed. Calling this with zero speed is not allowed.
+ * <p>
+ * <strong>audio fallback mode:</strong> select out-of-range parameter handling.
* <ul>
* <li> {@link PlaybackParams#AUDIO_FALLBACK_MODE_DEFAULT}:
* System will determine best handling. </li>
@@ -219,7 +231,8 @@
* @return this <code>Builder</code> instance.
* @throws IllegalArgumentException if the pitch is negative.
*/
- public @NonNull Builder setPitch(float pitch) {
+ public @NonNull Builder setPitch(
+ @FloatRange(from = 0.0f, to = Float.MAX_VALUE) float pitch) {
if (pitch < 0.f) {
throw new IllegalArgumentException("pitch must not be negative");
}
@@ -236,7 +249,14 @@
*
* @return this <code>Builder</code> instance.
*/
- public @NonNull Builder setSpeed(float speed) {
+ public @NonNull Builder setSpeed(
+ @FloatRange(from = 0.0f, to = Float.MAX_VALUE, fromInclusive = false) float speed) {
+ if (speed == 0.f) {
+ throw new IllegalArgumentException("0 speed is not allowed.");
+ }
+ if (speed < 0.f) {
+ throw new IllegalArgumentException("negative speed is not supported.");
+ }
if (Build.VERSION.SDK_INT >= 23) {
mPlaybackParams.setSpeed(speed);
} else {
diff --git a/media2/src/main/java/androidx/media2/UriMediaItem.java b/media2/src/main/java/androidx/media2/UriMediaItem.java
index a867f40..f7d55b6 100644
--- a/media2/src/main/java/androidx/media2/UriMediaItem.java
+++ b/media2/src/main/java/androidx/media2/UriMediaItem.java
@@ -16,7 +16,6 @@
package androidx.media2;
-import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
@@ -55,9 +54,6 @@
@NonParcelField
@SuppressWarnings("WeakerAccess") /* synthetic access */
List<HttpCookie> mUriCookies;
- @NonParcelField
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- Context mUriContext;
/**
* Used for VersionedParcelable
@@ -71,7 +67,6 @@
mUri = builder.mUri;
mUriHeader = builder.mUriHeader;
mUriCookies = builder.mUriCookies;
- mUriContext = builder.mUriContext;
}
/**
@@ -105,14 +100,6 @@
}
/**
- * Return the Context used for resolving the Uri of this media item.
- * @return the Context used for resolving the Uri of this media item
- */
- public @NonNull Context getUriContext() {
- return mUriContext;
- }
-
- /**
* This Builder class simplifies the creation of a {@link UriMediaItem} object.
*/
public static final class Builder extends MediaItem.Builder {
@@ -123,17 +110,14 @@
Map<String, String> mUriHeader;
@SuppressWarnings("WeakerAccess") /* synthetic access */
List<HttpCookie> mUriCookies;
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- Context mUriContext;
/**
* Creates a new Builder object with a content Uri.
*
- * @param context the Context to use when resolving the Uri
* @param uri the Content URI of the data you want to play
*/
- public Builder(@NonNull Context context, @NonNull Uri uri) {
- this(context, uri, null, null);
+ public Builder(@NonNull Uri uri) {
+ this(uri, null, null);
}
/**
@@ -147,7 +131,6 @@
* "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value to
* disallow or allow cross domain redirection.
*
- * @param context the Context to use when resolving the Uri
* @param uri the Content URI of the data you want to play
* @param headers the headers to be sent together with the request for the data
* The headers must not include cookies. Instead, use the cookies param.
@@ -155,12 +138,10 @@
* @throws IllegalArgumentException if the cookie handler is not of CookieManager type
* when cookies are provided.
*/
- public Builder(@NonNull Context context, @NonNull Uri uri,
- @Nullable Map<String, String> headers, @Nullable List<HttpCookie> cookies) {
- Preconditions.checkNotNull(context, "context cannot be null");
+ public Builder(@NonNull Uri uri, @Nullable Map<String, String> headers,
+ @Nullable List<HttpCookie> cookies) {
Preconditions.checkNotNull(uri, "uri cannot be null");
mUri = uri;
- mUriContext = context;
if (cookies != null) {
CookieHandler cookieHandler = CookieHandler.getDefault();
if (cookieHandler != null && !(cookieHandler instanceof CookieManager)) {
@@ -177,7 +158,6 @@
if (cookies != null) {
mUriCookies = new ArrayList<HttpCookie>(cookies);
}
- mUriContext = context;
}
// Override just to change return type.
diff --git a/media2/src/main/java/androidx/media2/exoplayer/ExoPlayerWrapper.java b/media2/src/main/java/androidx/media2/exoplayer/ExoPlayerWrapper.java
index 69052ca..59235e0 100644
--- a/media2/src/main/java/androidx/media2/exoplayer/ExoPlayerWrapper.java
+++ b/media2/src/main/java/androidx/media2/exoplayer/ExoPlayerWrapper.java
@@ -164,6 +164,7 @@
private final Runnable mPollBufferRunnable;
private SimpleExoPlayer mPlayer;
+ private Handler mPlayerHandler;
private DefaultAudioSink mAudioSink;
private TrackSelector mTrackSelector;
private MediaItemQueue mMediaItemQueue;
@@ -323,9 +324,10 @@
public void setAudioAttributes(AudioAttributesCompat audioAttributes) {
mHasAudioAttributes = true;
mPlayer.setAudioAttributes(ExoPlayerUtils.getAudioAttributes(audioAttributes));
+
// Reset the audio session ID, as it gets cleared by setting audio attributes.
if (mAudioSessionId != C.AUDIO_SESSION_ID_UNSET) {
- mAudioSink.setAudioSessionId(mAudioSessionId);
+ updatePlayerAudioSessionId(mPlayerHandler, mAudioSink, mAudioSessionId);
}
}
@@ -336,7 +338,9 @@
public void setAudioSessionId(int audioSessionId) {
mAudioSessionId = audioSessionId;
- mAudioSink.setAudioSessionId(mAudioSessionId);
+ if (mPlayer != null) {
+ updatePlayerAudioSessionId(mPlayerHandler, mAudioSink, mAudioSessionId);
+ }
}
public int getAudioSessionId() {
@@ -465,6 +469,7 @@
mBandwidthMeter,
new AnalyticsCollector.Factory(),
mLooper);
+ mPlayerHandler = new Handler(mPlayer.getPlaybackLooper());
mMediaItemQueue = new MediaItemQueue(mContext, mPlayer, mListener);
mPlayer.addListener(listener);
// TODO(b/80232248): Switch to AnalyticsListener once default methods work.
@@ -795,6 +800,19 @@
}
}
+ private static void updatePlayerAudioSessionId(
+ Handler playerHandler,
+ final DefaultAudioSink audioSink,
+ final int audioSessionId) {
+ // DefaultAudioSink is not thread-safe, so post the update to the playback thread.
+ playerHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ audioSink.setAudioSessionId(audioSessionId);
+ }
+ });
+ }
+
private static final class MediaItemInfo {
final MediaItem mMediaItem;
diff --git a/media2/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaItemTest.java b/media2/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaItemTest.java
index 8a44064..b217031 100644
--- a/media2/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaItemTest.java
+++ b/media2/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaItemTest.java
@@ -83,7 +83,7 @@
public MediaItem create(Context context) {
final MediaMetadata testMetadata = new MediaMetadata.Builder()
.putString("MediaItemTest", "MediaItemTest").build();
- return new UriMediaItem.Builder(context, Uri.parse("test://test"))
+ return new UriMediaItem.Builder(Uri.parse("test://test"))
.setMetadata(testMetadata)
.setStartPosition(1)
.setEndPosition(1000)
diff --git a/preference/src/main/java/androidx/preference/PreferenceFragmentCompat.java b/preference/src/main/java/androidx/preference/PreferenceFragmentCompat.java
index 26af8f8..2efc8f3 100644
--- a/preference/src/main/java/androidx/preference/PreferenceFragmentCompat.java
+++ b/preference/src/main/java/androidx/preference/PreferenceFragmentCompat.java
@@ -604,8 +604,11 @@
} else if (preference instanceof MultiSelectListPreference) {
f = MultiSelectListPreferenceDialogFragmentCompat.newInstance(preference.getKey());
} else {
- throw new IllegalArgumentException("Tried to display dialog for unknown " +
- "preference type. Did you forget to override onDisplayPreferenceDialog()?");
+ throw new IllegalArgumentException(
+ "Cannot display dialog for an unknown Preference type: "
+ + preference.getClass().getSimpleName()
+ + ". Make sure to implement onPreferenceDisplayDialog() to handle "
+ + "displaying a custom dialog for this Preference.");
}
f.setTargetFragment(this, 0);
f.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
index f8e522e..9163de3 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
@@ -1314,20 +1314,20 @@
ensureLayoutState();
final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
- final int absDy = Math.abs(delta);
- updateLayoutState(layoutDirection, absDy, true, state);
+ final int absDelta = Math.abs(delta);
+ updateLayoutState(layoutDirection, absDelta, true, state);
collectPrefetchPositionsForLayoutState(state, mLayoutState, layoutPrefetchRegistry);
}
- int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
- if (getChildCount() == 0 || dy == 0) {
+ int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
+ if (getChildCount() == 0 || delta == 0) {
return 0;
}
ensureLayoutState();
mLayoutState.mRecycle = true;
- final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
- final int absDy = Math.abs(dy);
- updateLayoutState(layoutDirection, absDy, true, state);
+ final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
+ final int absDelta = Math.abs(delta);
+ updateLayoutState(layoutDirection, absDelta, true, state);
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
if (consumed < 0) {
@@ -1336,10 +1336,10 @@
}
return 0;
}
- final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
+ final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;
mOrientationHelper.offsetChildren(-scrolled);
if (DEBUG) {
- Log.d(TAG, "scroll req: " + dy + " scrolled: " + scrolled);
+ Log.d(TAG, "scroll req: " + delta + " scrolled: " + scrolled);
}
mLayoutState.mLastScrollDelta = scrolled;
return scrolled;
@@ -1979,7 +1979,6 @@
return null;
}
ensureLayoutState();
- ensureLayoutState();
final int maxScroll = (int) (MAX_SCROLL_FACTOR * mOrientationHelper.getTotalSpace());
updateLayoutState(layoutDir, maxScroll, false, state);
mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN;
diff --git a/room/common-java8/build.gradle b/room/common-java8/build.gradle
new file mode 100644
index 0000000..4bd7f60
--- /dev/null
+++ b/room/common-java8/build.gradle
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.SupportLibraryExtension;
+
+plugins {
+ id("SupportJavaLibraryPlugin")
+}
+
+sourceCompatibility = JavaVersion.VERSION_1_8
+targetCompatibility = JavaVersion.VERSION_1_8
+
+dependencies {
+ compile(ANDROIDX_ANNOTATION)
+}
+
+supportLibrary {
+ name = "Android Room-Common-Java8"
+ publish = false
+ mavenVersion = LibraryVersions.ROOM
+ mavenGroup = LibraryGroups.ROOM
+ inceptionYear = "2019"
+ description = "Android Room-Common-Java8"
+ url = SupportLibraryExtension.ARCHITECTURE_URL
+}
\ No newline at end of file
diff --git a/room/common-java8/src/main/java/androidx/room/util/SneakyThrow.java b/room/common-java8/src/main/java/androidx/room/util/SneakyThrow.java
new file mode 100644
index 0000000..21497d2
--- /dev/null
+++ b/room/common-java8/src/main/java/androidx/room/util/SneakyThrow.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2019 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 androidx.room.util;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/**
+ * Java 8 Sneaky Throw technique.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SneakyThrow {
+
+ /**
+ * Re-throws a checked exception as if it was a runtime exception without wrapping it.
+ *
+ * @param e the exception to re-throw.
+ */
+ public static void reThrow(@NonNull Exception e) {
+ sneakyThrow(e);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static <E extends Throwable> void sneakyThrow(@NonNull Throwable e) throws E {
+ throw (E) e;
+ }
+
+ private SneakyThrow() {
+
+ }
+}
diff --git a/room/compiler/SQLite.g4 b/room/compiler/SQLite.g4
index 8eb895d..ab60202 100644
--- a/room/compiler/SQLite.g4
+++ b/room/compiler/SQLite.g4
@@ -28,16 +28,16 @@
* https://github.com/bkiers/sqlite-parser
* Developed by : Bart Kiers, [email protected]
*/
-grammar SQLite;
+grammar SQLite; // For version 3.24.0 of SQLite
parse
: ( sql_stmt_list | error )* EOF
;
error
- : UNEXPECTED_CHAR
- {
- throw new RuntimeException("UNEXPECTED_CHAR=" + $UNEXPECTED_CHAR.text);
+ : UNEXPECTED_CHAR
+ {
+ throw new RuntimeException("UNEXPECTED_CHAR=" + $UNEXPECTED_CHAR.text);
}
;
@@ -51,7 +51,6 @@
| attach_stmt
| begin_stmt
| commit_stmt
- | compound_select_stmt
| create_index_stmt
| create_table_stmt
| create_trigger_stmt
@@ -64,14 +63,12 @@
| drop_table_stmt
| drop_trigger_stmt
| drop_view_stmt
- | factored_select_stmt
| insert_stmt
| pragma_stmt
| reindex_stmt
| release_stmt
| rollback_stmt
| savepoint_stmt
- | simple_select_stmt
| select_stmt
| update_stmt
| update_stmt_limited
@@ -79,18 +76,18 @@
;
alter_table_stmt
- : K_ALTER K_TABLE ( database_name '.' )? table_name
+ : K_ALTER K_TABLE ( schema_name '.' )? table_name
( K_RENAME K_TO new_table_name
| K_ADD K_COLUMN? column_def
)
;
analyze_stmt
- : K_ANALYZE ( database_name | table_or_index_name | database_name '.' table_or_index_name )?
+ : K_ANALYZE ( schema_name | table_or_index_name | schema_name '.' table_or_index_name )?
;
attach_stmt
- : K_ATTACH K_DATABASE? expr K_AS database_name
+ : K_ATTACH K_DATABASE? expr K_AS schema_name
;
begin_stmt
@@ -101,43 +98,36 @@
: ( K_COMMIT | K_END ) ( K_TRANSACTION transaction_name? )?
;
-compound_select_stmt
- : with_clause?
- select_core ( ( K_UNION K_ALL? | K_INTERSECT | K_EXCEPT ) select_core )+
- ( K_ORDER K_BY ordering_term ( ',' ordering_term )* )?
- ( K_LIMIT expr ( ( K_OFFSET | ',' ) expr )? )?
- ;
-
create_index_stmt
: K_CREATE K_UNIQUE? K_INDEX ( K_IF K_NOT K_EXISTS )?
- ( database_name '.' )? index_name K_ON table_name '(' indexed_column ( ',' indexed_column )* ')'
+ ( schema_name '.' )? index_name K_ON table_name '(' indexed_column ( ',' indexed_column )* ')'
( K_WHERE expr )?
;
create_table_stmt
: K_CREATE ( K_TEMP | K_TEMPORARY )? K_TABLE ( K_IF K_NOT K_EXISTS )?
- ( database_name '.' )? table_name
- ( '(' column_def ( ',' column_def )*? ( ',' table_constraint )* ')' ( K_WITHOUT IDENTIFIER )?
+ ( schema_name '.' )? table_name
+ ( '(' column_def ( ',' column_def )*? ( ',' table_constraint )* ')' WITHOUT_ROWID?
| K_AS select_stmt
)
;
create_trigger_stmt
: K_CREATE ( K_TEMP | K_TEMPORARY )? K_TRIGGER ( K_IF K_NOT K_EXISTS )?
- ( database_name '.' )? trigger_name ( K_BEFORE | K_AFTER | K_INSTEAD K_OF )?
- ( K_DELETE | K_INSERT | K_UPDATE ( K_OF column_name ( ',' column_name )* )? ) K_ON ( database_name '.' )? table_name
+ ( schema_name '.' )? trigger_name ( K_BEFORE | K_AFTER | K_INSTEAD K_OF )?
+ ( K_DELETE | K_INSERT | K_UPDATE ( K_OF column_name ( ',' column_name )* )? ) K_ON ( schema_name '.' )? table_name
( K_FOR K_EACH K_ROW )? ( K_WHEN expr )?
K_BEGIN ( ( update_stmt | insert_stmt | delete_stmt | select_stmt ) ';' )+ K_END
;
create_view_stmt
: K_CREATE ( K_TEMP | K_TEMPORARY )? K_VIEW ( K_IF K_NOT K_EXISTS )?
- ( database_name '.' )? view_name K_AS select_stmt
+ ( schema_name '.' )? view_name ( column_name ( ',' column_name )* )? K_AS select_stmt
;
create_virtual_table_stmt
: K_CREATE K_VIRTUAL K_TABLE ( K_IF K_NOT K_EXISTS )?
- ( database_name '.' )? table_name
+ ( schema_name '.' )? table_name
K_USING module_name ( '(' module_argument ( ',' module_argument )* ')' )?
;
@@ -149,36 +139,27 @@
delete_stmt_limited
: with_clause? K_DELETE K_FROM qualified_table_name
( K_WHERE expr )?
- ( ( K_ORDER K_BY ordering_term ( ',' ordering_term )* )?
- K_LIMIT expr ( ( K_OFFSET | ',' ) expr )?
- )?
+ ( order_clause? limit_clause )?
;
detach_stmt
- : K_DETACH K_DATABASE? database_name
+ : K_DETACH K_DATABASE? schema_name
;
drop_index_stmt
- : K_DROP K_INDEX ( K_IF K_EXISTS )? ( database_name '.' )? index_name
+ : K_DROP K_INDEX ( K_IF K_EXISTS )? ( schema_name '.' )? index_name
;
drop_table_stmt
- : K_DROP K_TABLE ( K_IF K_EXISTS )? ( database_name '.' )? table_name
+ : K_DROP K_TABLE ( K_IF K_EXISTS )? ( schema_name '.' )? table_name
;
drop_trigger_stmt
- : K_DROP K_TRIGGER ( K_IF K_EXISTS )? ( database_name '.' )? trigger_name
+ : K_DROP K_TRIGGER ( K_IF K_EXISTS )? ( schema_name '.' )? trigger_name
;
drop_view_stmt
- : K_DROP K_VIEW ( K_IF K_EXISTS )? ( database_name '.' )? view_name
- ;
-
-factored_select_stmt
- : with_clause?
- select_core ( compound_operator select_core )*
- ( K_ORDER K_BY ordering_term ( ',' ordering_term )* )?
- ( K_LIMIT expr ( ( K_OFFSET | ',' ) expr )? )?
+ : K_DROP K_VIEW ( K_IF K_EXISTS )? ( schema_name '.' )? view_name
;
insert_stmt
@@ -189,21 +170,30 @@
| K_INSERT K_OR K_ABORT
| K_INSERT K_OR K_FAIL
| K_INSERT K_OR K_IGNORE ) K_INTO
- ( database_name '.' )? table_name ( '(' column_name ( ',' column_name )* ')' )?
+ ( schema_name '.' )? table_name ( K_AS table_alias )? ( '(' column_name ( ',' column_name )* ')' )?
( K_VALUES '(' expr ( ',' expr )* ')' ( ',' '(' expr ( ',' expr )* ')' )*
| select_stmt
| K_DEFAULT K_VALUES
)
+ upsert_clause?
+ ;
+
+upsert_clause
+ : K_ON K_CONFLICT ( '(' indexed_column ( ',' indexed_column )* ')' ( K_WHERE expr )? )?
+ ( DO_NOTHING
+ | DO_UPDATE K_SET ( column_name | column_name_list ) '=' expr
+ ( ',' ( column_name | column_name_list ) '=' expr )*
+ ( K_WHERE expr )?
+ )
;
pragma_stmt
- : K_PRAGMA ( database_name '.' )? pragma_name ( '=' pragma_value
- | '(' pragma_value ')' )?
+ : K_PRAGMA ( schema_name '.' )? pragma_name ( '=' pragma_value | '(' pragma_value ')' )?
;
reindex_stmt
: K_REINDEX ( collation_name
- | ( database_name '.' )? ( table_name | index_name )
+ | ( schema_name '.' )? ( table_name | index_name )
)?
;
@@ -219,17 +209,11 @@
: K_SAVEPOINT savepoint_name
;
-simple_select_stmt
- : with_clause?
- select_core ( K_ORDER K_BY ordering_term ( ',' ordering_term )* )?
- ( K_LIMIT expr ( ( K_OFFSET | ',' ) expr )? )?
- ;
-
select_stmt
: with_clause?
select_or_values ( compound_operator select_or_values )*
- ( K_ORDER K_BY ordering_term ( ',' ordering_term )* )?
- ( K_LIMIT expr ( ( K_OFFSET | ',' ) expr )? )?
+ order_clause?
+ limit_clause?
;
select_or_values
@@ -246,7 +230,8 @@
| K_OR K_REPLACE
| K_OR K_FAIL
| K_OR K_IGNORE )? qualified_table_name
- K_SET column_name '=' expr ( ',' column_name '=' expr )* ( K_WHERE expr )?
+ K_SET ( column_name | column_name_list ) '=' expr ( ',' ( column_name | column_name_list ) '=' expr )*
+ ( K_WHERE expr )?
;
update_stmt_limited
@@ -255,14 +240,13 @@
| K_OR K_REPLACE
| K_OR K_FAIL
| K_OR K_IGNORE )? qualified_table_name
- K_SET column_name '=' expr ( ',' column_name '=' expr )* ( K_WHERE expr )?
- ( ( K_ORDER K_BY ordering_term ( ',' ordering_term )* )?
- K_LIMIT expr ( ( K_OFFSET | ',' ) expr )?
- )?
+ K_SET ( column_name | column_name_list ) '=' expr ( ',' ( column_name | column_name_list ) '=' expr )*
+ ( K_WHERE expr )?
+ ( order_clause? limit_clause )?
;
vacuum_stmt
- : K_VACUUM
+ : K_VACUUM schema_name?
;
column_def
@@ -271,7 +255,7 @@
type_name
: name+? ( '(' signed_number ')'
- | '(' signed_number ',' signed_number ')' )?
+ | '(' signed_number ',' signed_number ')' )?
;
column_constraint
@@ -296,45 +280,23 @@
)?
;
-/*
- SQLite understands the following binary operators, in order from highest to
- lowest precedence:
-
- ||
- * / %
- + -
- << >> & |
- < <= > >=
- = == != <> IS IS NOT IN LIKE GLOB MATCH REGEXP
- AND
- OR
-*/
expr
: literal_value
| BIND_PARAMETER
- | ( ( database_name '.' )? table_name '.' )? column_name
+ | ( ( schema_name '.' )? table_name '.' )? column_name
| unary_operator expr
- | expr '||' expr
- | expr ( '*' | '/' | '%' ) expr
- | expr ( '+' | '-' ) expr
- | expr ( '<<' | '>>' | '&' | '|' ) expr
- | expr ( '<' | '<=' | '>' | '>=' ) expr
- | expr ( '=' | '==' | '!=' | '<>' ) expr
- | expr K_AND expr
- | expr K_OR expr
+ | expr binary_operator expr
| function_name '(' ( K_DISTINCT? expr ( ',' expr )* | '*' )? ')'
- | '(' expr ')'
+ | '(' expr ( ',' expr )* ')'
| K_CAST '(' expr K_AS type_name ')'
| expr K_COLLATE collation_name
| expr K_NOT? ( K_LIKE | K_GLOB | K_REGEXP | K_MATCH ) expr ( K_ESCAPE expr )?
| expr ( K_ISNULL | K_NOTNULL | K_NOT K_NULL )
| expr K_IS K_NOT? expr
| expr K_NOT? K_BETWEEN expr K_AND expr
- | expr K_NOT? K_IN ( '(' ( select_stmt
- | expr ( ',' expr )*
- )?
- ')'
- | ( database_name '.' )? table_name )
+ | expr K_NOT? K_IN ( '(' ( select_stmt | expr ( ',' expr )* )? ')'
+ | ( schema_name '.' )? table_name
+ | ( schema_name '.' )? table_function '(' ( expr ( ',' expr )* )? ')' )
| ( ( K_NOT )? K_EXISTS )? '(' select_stmt ')'
| K_CASE expr? ( K_WHEN expr K_THEN expr )+ ( K_ELSE expr )? K_END
| raise_function
@@ -360,7 +322,7 @@
;
indexed_column
- : column_name ( K_COLLATE collation_name )? ( K_ASC | K_DESC )?
+ : ( column_name | expr ) ( K_COLLATE collation_name )? ( K_ASC | K_DESC )?
;
table_constraint
@@ -375,23 +337,32 @@
: K_WITH K_RECURSIVE? common_table_expression ( ',' common_table_expression )*
;
+common_table_expression
+ : table_name ( '(' column_name ( ',' column_name )* ')' )? K_AS '(' select_stmt ')'
+ ;
+
qualified_table_name
- : ( database_name '.' )? table_name ( K_INDEXED K_BY index_name
- | K_NOT K_INDEXED )?
+ : ( schema_name '.' )? table_name ( K_AS table_alias )?
+ ( K_INDEXED K_BY index_name | K_NOT K_INDEXED )?
+ ;
+
+order_clause
+ : K_ORDER K_BY ordering_term ( ',' ordering_term )*
;
ordering_term
: expr ( K_COLLATE collation_name )? ( K_ASC | K_DESC )?
;
+limit_clause
+ : K_LIMIT expr ( ( K_OFFSET | ',' ) expr )?
+ ;
+
pragma_value
: signed_number
| name
| STRING_LITERAL
- ;
-
-common_table_expression
- : table_name ( '(' column_name ( ',' column_name )* ')' )? K_AS '(' select_stmt ')'
+ | boolean_literal
;
result_column
@@ -402,12 +373,9 @@
table_or_subquery
: ( schema_name '.' )? table_name ( K_AS? table_alias )?
- ( K_INDEXED K_BY index_name
- | K_NOT K_INDEXED )?
- | ( schema_name '.' )? table_function_name '(' ( expr ( ',' expr )* )? ')' ( K_AS? table_alias )?
- | '(' ( table_or_subquery ( ',' table_or_subquery )*
- | join_clause )
- ')'
+ ( K_INDEXED K_BY index_name | K_NOT K_INDEXED )?
+ | ( schema_name '.' )? table_function '(' ( expr ( ',' expr )* )? ')' ( K_AS? table_alias )?
+ | '(' ( table_or_subquery ( ',' table_or_subquery )* | join_clause ) ')'
| '(' select_stmt ')' ( K_AS? table_alias )?
;
@@ -425,14 +393,6 @@
| K_USING '(' column_name ( ',' column_name )* ')' )?
;
-select_core
- : K_SELECT ( K_DISTINCT | K_ALL )? result_column ( ',' result_column )*
- ( K_FROM ( table_or_subquery ( ',' table_or_subquery )* | join_clause ) )?
- ( K_WHERE expr )?
- ( K_GROUP K_BY expr ( ',' expr )* ( K_HAVING expr )? )?
- | K_VALUES '(' expr ( ',' expr )* ')' ( ',' '(' expr ( ',' expr )* ')' )*
- ;
-
compound_operator
: K_UNION
| K_UNION K_ALL
@@ -452,6 +412,12 @@
| K_CURRENT_TIME
| K_CURRENT_DATE
| K_CURRENT_TIMESTAMP
+ | boolean_literal
+ ;
+
+boolean_literal
+ : TRUE
+ | FALSE
;
unary_operator
@@ -461,6 +427,33 @@
| K_NOT
;
+/*
+ SQLite understands the following binary operators, in order from highest to
+ lowest precedence:
+
+ ||
+ * / %
+ + -
+ << >> & |
+ < <= > >=
+ = == != <> IS IS NOT IN LIKE GLOB MATCH REGEXP
+ AND
+ OR
+
+ This rule is only used in `expr`, which has more complete directives for `IS` through `REGEXP`,
+ so we leave them out here.
+*/
+binary_operator
+ : '||'
+ | ( '*' | '/' | '%' )
+ | ( '+' | '-' )
+ | ( '<<' | '>>' | '&' | '|' )
+ | ( '<' | '<=' | '>' | '>=' )
+ | ( '=' | '==' | '!=' | '<>' )
+ | K_AND
+ | K_OR
+ ;
+
error_message
: STRING_LITERAL
;
@@ -475,6 +468,10 @@
| STRING_LITERAL
;
+column_name_list
+ : '(' column_name ( ',' column_name )* ')'
+ ;
+
keyword
: K_ABORT
| K_ACTION
@@ -612,15 +609,11 @@
: any_name
;
-database_name
- : any_name
- ;
-
schema_name
: any_name
;
-table_function_name
+table_function
: any_name
;
@@ -713,6 +706,8 @@
EQ : '==';
NOT_EQ1 : '!=';
NOT_EQ2 : '<>';
+TRUE : T R U E;
+FALSE : F A L S E;
// http://www.sqlite.org/lang_keywords.html
K_ABORT : A B O R T;
@@ -840,16 +835,22 @@
K_WITH : W I T H;
K_WITHOUT : W I T H O U T;
+// These are not keywords, but their constituents might be wrongly matched as identifiers.
+WITHOUT_ROWID: K_WITHOUT SPACES R O W I D;
+DO_NOTHING: D O SPACES N O T H I N G;
+DO_UPDATE: D O SPACES K_UPDATE;
+
IDENTIFIER
: '"' (~'"' | '""')* '"'
| '`' (~'`' | '``')* '`'
| '[' ~']'* ']'
- | [a-zA-Z_] [a-zA-Z_0-9]* // TODO check: needs more chars in set
+ | [a-zA-Z_\u00a1-\uffff] [a-zA-Z_0-9\u00a1-\uffff]*
;
NUMERIC_LITERAL
: DIGIT+ ( '.' DIGIT* )? ( E [-+]? DIGIT+ )?
| '.' DIGIT+ ( E [-+]? DIGIT+ )?
+ | '0' X HEXDIGIT+
;
BIND_PARAMETER
@@ -882,6 +883,7 @@
;
fragment DIGIT : [0-9];
+fragment HEXDIGIT : [0-9a-fA-F];
fragment A : [aA];
fragment B : [bB];
diff --git a/room/compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt b/room/compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt
index dba9aa7..bb93c23 100644
--- a/room/compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt
@@ -51,16 +51,11 @@
private fun findQueryType(statement: ParseTree): QueryType {
return when (statement) {
- is SQLiteParser.Factored_select_stmtContext,
- is SQLiteParser.Compound_select_stmtContext,
- is SQLiteParser.Select_stmtContext,
- is SQLiteParser.Simple_select_stmtContext ->
+ is SQLiteParser.Select_stmtContext ->
QueryType.SELECT
-
is SQLiteParser.Delete_stmt_limitedContext,
is SQLiteParser.Delete_stmtContext ->
QueryType.DELETE
-
is SQLiteParser.Insert_stmtContext ->
QueryType.INSERT
is SQLiteParser.Update_stmtContext,
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt b/room/compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
index 8dc5742..4d8cfa7 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
@@ -201,9 +201,10 @@
adapter = context.typeAdapterStore.findPreparedQueryResultAdapter(returnType, query)
) { callableImpl, dbField ->
addStatement(
- "return $T.execute($N, $L, $N)",
+ "return $T.execute($N, $L, $L, $N)",
RoomCoroutinesTypeNames.COROUTINES_ROOM,
dbField,
+ "true", // inTransaction
callableImpl,
continuationParam.simpleName.toString()
)
@@ -217,9 +218,10 @@
adapter = context.typeAdapterStore.findInsertAdapter(returnType, params)
) { callableImpl, dbField ->
addStatement(
- "return $T.execute($N, $L, $N)",
+ "return $T.execute($N, $L, $L, $N)",
RoomCoroutinesTypeNames.COROUTINES_ROOM,
dbField,
+ "true", // inTransaction
callableImpl,
continuationParam.simpleName.toString()
)
@@ -231,9 +233,10 @@
adapter = context.typeAdapterStore.findDeleteOrUpdateAdapter(returnType)
) { callableImpl, dbField ->
addStatement(
- "return $T.execute($N, $L, $N)",
+ "return $T.execute($N, $L, $L, $N)",
RoomCoroutinesTypeNames.COROUTINES_ROOM,
dbField,
+ "true", // inTransaction
callableImpl,
continuationParam.simpleName.toString()
)
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/prepared/binderprovider/GuavaListenableFuturePreparedQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/androidx/room/solver/prepared/binderprovider/GuavaListenableFuturePreparedQueryResultBinderProvider.kt
index 95e4e71..54b3d23 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/prepared/binderprovider/GuavaListenableFuturePreparedQueryResultBinderProvider.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/prepared/binderprovider/GuavaListenableFuturePreparedQueryResultBinderProvider.kt
@@ -52,9 +52,10 @@
adapter = context.typeAdapterStore.findPreparedQueryResultAdapter(typeArg, query)
) { callableImpl, dbField ->
addStatement(
- "return $T.createListenableFuture($N, $L)",
+ "return $T.createListenableFuture($N, $L, $L)",
RoomGuavaTypeNames.GUAVA_ROOM,
dbField,
+ "true", // inTransaction
callableImpl
)
}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/CoroutineResultBinder.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/CoroutineResultBinder.kt
index b59ecc0..d97ba61 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/CoroutineResultBinder.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/CoroutineResultBinder.kt
@@ -57,9 +57,10 @@
scope.builder().apply {
addStatement(
- "return $T.execute($N, $L, $N)",
+ "return $T.execute($N, $L, $L, $N)",
RoomCoroutinesTypeNames.COROUTINES_ROOM,
dbField,
+ if (inTransaction) "true" else "false",
callableImpl,
continuationParamName)
}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaListenableFutureQueryResultBinder.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaListenableFutureQueryResultBinder.kt
index 7141786..722242e 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaListenableFutureQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaListenableFutureQueryResultBinder.kt
@@ -56,9 +56,10 @@
scope.builder().apply {
addStatement(
- "return $T.createListenableFuture($N, $L, $L, $L)",
+ "return $T.createListenableFuture($N, $L, $L, $L, $L)",
RoomGuavaTypeNames.GUAVA_ROOM,
dbField,
+ if (inTransaction) "true" else "false",
callableImpl,
roomSQLiteQueryVar,
canReleaseQuery
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/LiveDataQueryResultBinder.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/LiveDataQueryResultBinder.kt
index 2eebcda5..cd84015 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/LiveDataQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/LiveDataQueryResultBinder.kt
@@ -67,8 +67,12 @@
scope.builder().apply {
val tableNamesList = tableNames.joinToString(",") { "\"$it\"" }
addStatement(
- "return $N.getInvalidationTracker().createLiveData(new $T{$L}, $L)",
- dbField, String::class.arrayTypeName(), tableNamesList, callableImpl
+ "return $N.getInvalidationTracker().createLiveData(new $T{$L}, $L, $L)",
+ dbField,
+ String::class.arrayTypeName(),
+ tableNamesList,
+ if (inTransaction) "true" else "false",
+ callableImpl
)
}
}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/RxQueryResultBinder.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/RxQueryResultBinder.kt
index 7b2e54b..d52e804 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/RxQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/RxQueryResultBinder.kt
@@ -58,9 +58,14 @@
}.build()
scope.builder().apply {
val tableNamesList = queryTableNames.joinToString(",") { "\"$it\"" }
- addStatement("return $T.$N($N, new $T{$L}, $L)",
- RoomRxJava2TypeNames.RX_ROOM, rxType.methodName, dbField,
- String::class.arrayTypeName(), tableNamesList, callableImpl)
+ addStatement("return $T.$N($N, $L, new $T{$L}, $L)",
+ RoomRxJava2TypeNames.RX_ROOM,
+ rxType.methodName,
+ dbField,
+ if (inTransaction) "true" else "false",
+ String::class.arrayTypeName(),
+ tableNamesList,
+ callableImpl)
}
}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureDeleteOrUpdateMethodBinderProvider.kt b/room/compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureDeleteOrUpdateMethodBinderProvider.kt
index b43e23c..106bc14 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureDeleteOrUpdateMethodBinderProvider.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureDeleteOrUpdateMethodBinderProvider.kt
@@ -54,9 +54,10 @@
val adapter = context.typeAdapterStore.findDeleteOrUpdateAdapter(typeArg)
return createDeleteOrUpdateBinder(typeArg, adapter) { callableImpl, dbField ->
addStatement(
- "return $T.createListenableFuture($N, $L)",
+ "return $T.createListenableFuture($N, $L, $L)",
RoomGuavaTypeNames.GUAVA_ROOM,
dbField,
+ "true", // inTransaction
callableImpl
)
}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureInsertMethodBinderProvider.kt b/room/compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureInsertMethodBinderProvider.kt
index 7b3cb47..f603510 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureInsertMethodBinderProvider.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureInsertMethodBinderProvider.kt
@@ -58,9 +58,10 @@
val adapter = context.typeAdapterStore.findInsertAdapter(typeArg, params)
return createInsertBinder(typeArg, adapter) { callableImpl, dbField ->
addStatement(
- "return $T.createListenableFuture($N, $L)",
+ "return $T.createListenableFuture($N, $L, $L)",
RoomGuavaTypeNames.GUAVA_ROOM,
dbField,
+ "true", // inTransaction
callableImpl
)
}
diff --git a/room/compiler/src/test/data/daoWriter/output/ComplexDao.java b/room/compiler/src/test/data/daoWriter/output/ComplexDao.java
index bb945a3..f047ecd4 100644
--- a/room/compiler/src/test/data/daoWriter/output/ComplexDao.java
+++ b/room/compiler/src/test/data/daoWriter/output/ComplexDao.java
@@ -281,7 +281,7 @@
final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);
int _argIndex = 1;
_statement.bindLong(_argIndex, id);
- return __db.getInvalidationTracker().createLiveData(new String[]{"user"}, new Callable<User>() {
+ return __db.getInvalidationTracker().createLiveData(new String[]{"user"}, false, new Callable<User>() {
@Override
public User call() throws Exception {
final Cursor _cursor = DBUtil.query(__db, _statement, false);
@@ -330,7 +330,7 @@
_statement.bindLong(_argIndex, _item);
_argIndex ++;
}
- return __db.getInvalidationTracker().createLiveData(new String[]{"user"}, new Callable<List<User>>() {
+ return __db.getInvalidationTracker().createLiveData(new String[]{"user"}, false, new Callable<List<User>>() {
@Override
public List<User> call() throws Exception {
final Cursor _cursor = DBUtil.query(__db, _statement, false);
diff --git a/room/compiler/src/test/kotlin/androidx/room/parser/SqlParserTest.kt b/room/compiler/src/test/kotlin/androidx/room/parser/SqlParserTest.kt
index 885f7bd..f939e26 100644
--- a/room/compiler/src/test/kotlin/androidx/room/parser/SqlParserTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/parser/SqlParserTest.kt
@@ -66,6 +66,17 @@
}
@Test
+ fun upsertQuery() {
+ val parsed = SqlParser.parse(
+ "INSERT INTO notes (id, content) VALUES (:id, :content) " +
+ "ON CONFLICT (id) DO UPDATE SET content = excluded.content, " +
+ "revision = revision + 1, modifiedTime = strftime('%s','now')"
+ )
+ assertThat(parsed.errors, `is`(emptyList()))
+ assertThat(parsed.type, `is`(QueryType.INSERT))
+ }
+
+ @Test
fun explain() {
assertErrors("EXPLAIN QUERY PLAN SELECT * FROM users",
ParserErrors.invalidQueryType(QueryType.EXPLAIN))
@@ -140,6 +151,19 @@
}
@Test
+ fun unicodeInIdentifiers() {
+ val query = SqlParser.parse("SELECT 名, 色 FROM 猫")
+ assertThat(query.errors, `is`(emptyList()))
+ assertThat(query.tables, `is`(setOf(Table("猫", "猫"))))
+ }
+
+ @Test
+ fun rowValue_where() {
+ val query = SqlParser.parse("SELECT * FROM notes WHERE (id, content) > (:id, :content)")
+ assertThat(query.errors, `is`(emptyList()))
+ }
+
+ @Test
fun findBindVariables() {
assertVariables("select * from users")
assertVariables("select * from users where name like ?", "?")
diff --git a/room/guava/src/main/java/androidx/room/guava/GuavaRoom.java b/room/guava/src/main/java/androidx/room/guava/GuavaRoom.java
index d4f6c2f..4e2f500 100644
--- a/room/guava/src/main/java/androidx/room/guava/GuavaRoom.java
+++ b/room/guava/src/main/java/androidx/room/guava/GuavaRoom.java
@@ -48,8 +48,8 @@
* Returns a {@link ListenableFuture<T>} created by submitting the input {@code callable} to
* {@link ArchTaskExecutor}'s background-threaded Executor.
*
- * @deprecated
- * Use {@link #createListenableFuture(RoomDatabase, Callable, RoomSQLiteQuery, boolean)}
+ * @deprecated Use {@link #createListenableFuture(RoomDatabase, boolean, Callable,
+ * RoomSQLiteQuery, boolean)}
*/
@Deprecated
public static <T> ListenableFuture<T> createListenableFuture(
@@ -63,7 +63,11 @@
/**
* Returns a {@link ListenableFuture<T>} created by submitting the input {@code callable} to
* {@link RoomDatabase}'s {@link java.util.concurrent.Executor}.
+ *
+ * @deprecated Use {@link #createListenableFuture(RoomDatabase, boolean, Callable,
+ * RoomSQLiteQuery, boolean)}
*/
+ @Deprecated
public static <T> ListenableFuture<T> createListenableFuture(
final RoomDatabase roomDatabase,
final Callable<T> callable,
@@ -73,6 +77,20 @@
roomDatabase.getQueryExecutor(), callable, query, releaseQuery);
}
+ /**
+ * Returns a {@link ListenableFuture<T>} created by submitting the input {@code callable} to
+ * {@link RoomDatabase}'s {@link java.util.concurrent.Executor}.
+ */
+ public static <T> ListenableFuture<T> createListenableFuture(
+ final RoomDatabase roomDatabase,
+ final boolean inTransaction,
+ final Callable<T> callable,
+ final RoomSQLiteQuery query,
+ final boolean releaseQuery) {
+ return createListenableFuture(
+ getExecutor(roomDatabase, inTransaction), callable, query, releaseQuery);
+ }
+
private static <T> ListenableFuture<T> createListenableFuture(
final Executor executor,
final Callable<T> callable,
@@ -104,12 +122,34 @@
/**
* Returns a {@link ListenableFuture<T>} created by submitting the input {@code callable} to
* {@link RoomDatabase}'s {@link java.util.concurrent.Executor}.
+ *
+ * @deprecated Use {@link #createListenableFuture(RoomDatabase, boolean, Callable)}
*/
+ @Deprecated
public static <T> ListenableFuture<T> createListenableFuture(
final RoomDatabase roomDatabase,
final Callable<T> callable) {
+ return createListenableFuture(roomDatabase, false, callable);
+ }
+
+ /**
+ * Returns a {@link ListenableFuture<T>} created by submitting the input {@code callable} to
+ * {@link RoomDatabase}'s {@link java.util.concurrent.Executor}.
+ */
+ public static <T> ListenableFuture<T> createListenableFuture(
+ final RoomDatabase roomDatabase,
+ final boolean inTransaction,
+ final Callable<T> callable) {
ListenableFutureTask<T> listenableFutureTask = ListenableFutureTask.create(callable);
- roomDatabase.getQueryExecutor().execute(listenableFutureTask);
+ getExecutor(roomDatabase, inTransaction).execute(listenableFutureTask);
return listenableFutureTask;
}
+
+ private static Executor getExecutor(RoomDatabase database, boolean inTransaction) {
+ if (inTransaction) {
+ return database.getTransactionExecutor();
+ } else {
+ return database.getQueryExecutor();
+ }
+ }
}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SneakyThrowTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SneakyThrowTest.kt
new file mode 100644
index 0000000..f462d00
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SneakyThrowTest.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2019 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 androidx.room.integration.kotlintestapp.test
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import org.json.JSONException
+import org.json.JSONObject
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.Callable
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SneakyThrowTest : TestDatabaseTest() {
+
+ @Test
+ fun testCheckedException() {
+ try {
+ database.runInTransaction(Callable<String> {
+ val json = JSONObject()
+ json.getString("key") // method declares that it throws JSONException
+ })
+ fail("runInTransaction should have thrown an exception")
+ } catch (ex: JSONException) {
+ // no-op on purpose
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt
index 901f410..b8e81fa 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt
@@ -17,6 +17,7 @@
package androidx.room.integration.kotlintestapp.test
import android.os.Build
+import androidx.arch.core.executor.ArchTaskExecutor
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.integration.kotlintestapp.NewThreadDispatcher
@@ -45,8 +46,10 @@
import org.junit.runner.RunWith
import java.io.IOException
import java.util.concurrent.CountDownLatch
+import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicInteger
@LargeTest
@RunWith(AndroidJUnit4::class)
@@ -633,6 +636,52 @@
@Test
@Suppress("DeferredResultUnused")
+ fun withTransaction_multipleTransactions_verifyThreadUsage() {
+ val busyThreadsCount = AtomicInteger()
+ // Executor wrapper that counts threads that are busy executing commands.
+ class WrappedService(val delegate: ExecutorService) : ExecutorService by delegate {
+ override fun execute(command: Runnable) {
+ delegate.execute {
+ busyThreadsCount.incrementAndGet()
+ try {
+ command.run()
+ } finally {
+ busyThreadsCount.decrementAndGet()
+ }
+ }
+ }
+ }
+ val wrappedExecutor = WrappedService(Executors.newCachedThreadPool())
+ val localDatabase = Room.inMemoryDatabaseBuilder(
+ ApplicationProvider.getApplicationContext(), TestDatabase::class.java)
+ .setQueryExecutor(ArchTaskExecutor.getIOThreadExecutor())
+ .setTransactionExecutor(wrappedExecutor)
+ .build()
+
+ // Run two parallel transactions but verify that only 1 thread is busy when the transactions
+ // execute, indicating that threads are not busy waiting on sql connections but are instead
+ // suspended.
+ runBlocking(Dispatchers.IO) {
+ async {
+ localDatabase.withTransaction {
+ delay(200) // delay a bit to let the other transaction proceed
+ assertThat(busyThreadsCount.get()).isEqualTo(1)
+ }
+ }
+
+ async {
+ localDatabase.withTransaction {
+ delay(200) // delay a bit to let the other transaction proceed
+ assertThat(busyThreadsCount.get()).isEqualTo(1)
+ }
+ }
+ }
+
+ wrappedExecutor.awaitTermination(1, TimeUnit.SECONDS)
+ }
+
+ @Test
+ @Suppress("DeferredResultUnused")
fun withTransaction_leakTransactionContext_async() {
runBlocking {
val leakedContext = database.withTransaction {
@@ -700,7 +749,7 @@
val executorService = Executors.newSingleThreadExecutor()
val localDatabase = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(), TestDatabase::class.java)
- .setQueryExecutor(executorService)
+ .setTransactionExecutor(executorService)
.build()
// Simulate a busy executor, no thread to acquire for transaction.
@@ -735,7 +784,7 @@
val executorService = Executors.newCachedThreadPool()
val localDatabase = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(), TestDatabase::class.java)
- .setQueryExecutor(executorService)
+ .setTransactionExecutor(executorService)
.build()
executorService.shutdownNow()
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/InvalidationTrackerTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/InvalidationTrackerTest.java
index d49779a..6b88e33 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/InvalidationTrackerTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/InvalidationTrackerTest.java
@@ -125,7 +125,7 @@
public void createLiveData() throws ExecutionException, InterruptedException, TimeoutException {
final LiveData<Item> liveData = mDb
.getInvalidationTracker()
- .createLiveData(new String[]{"Item"}, () -> mDb.getItemDao().itemById(1));
+ .createLiveData(new String[]{"Item"}, false, () -> mDb.getItemDao().itemById(1));
mDb.getItemDao().insert(new Item(1, "v1"));
@@ -147,7 +147,7 @@
throws ExecutionException, InterruptedException, TimeoutException {
LiveData<Item> liveData = mDb
.getInvalidationTracker()
- .createLiveData(new String[]{"Item"}, () -> mDb.getItemDao().itemById(1));
+ .createLiveData(new String[]{"Item"}, false, () -> mDb.getItemDao().itemById(1));
mDb.getItemDao().insert(new Item(1, "v1"));
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/SneakyThrowTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/SneakyThrowTest.java
new file mode 100644
index 0000000..ca96fdc
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/SneakyThrowTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2019 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 androidx.room.integration.testapp.test;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import androidx.test.filters.SmallTest;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public class SneakyThrowTest extends TestDatabaseTest {
+
+ @Test
+ public void testRuntimeException_catchRuntimeException() {
+ try {
+ mDatabase.runInTransaction(() -> {
+ throw new IllegalStateException("Boom");
+ });
+ fail("runInTransaction should have thrown an exception");
+ } catch (IllegalStateException e) {
+ // no-op on purpose
+ }
+ }
+
+ @Test
+ public void testCheckedException_catchThrowable() {
+ try {
+ mDatabase.runInTransaction(() -> {
+ JSONObject json = new JSONObject();
+ return json.getString("key"); // method declares that it throws JSONException
+ });
+ fail("runInTransaction should have thrown an exception");
+ } catch (Throwable e) {
+ assertTrue(e instanceof JSONException);
+ }
+ }
+
+ @Test
+ public void testCheckedException_catchException() {
+ try {
+ mDatabase.runInTransaction(() -> {
+ JSONObject json = new JSONObject();
+ return json.getString("key"); // method declares that it throws JSONException
+ });
+ fail("runInTransaction should have thrown an exception");
+ } catch (Exception e) {
+ assertTrue(e instanceof JSONException);
+ }
+ }
+
+ @Test
+ public void testCheckedException_catchCheckedException() {
+ try {
+ // Must move the lambda to a method that declares throwing the checked exception,
+ // otherwise compiler complains about 'exception not thrown in corresponding block', a
+ // limitation of the sneaky throw technique.
+ doJsonWork();
+ fail("doJsonWork should have thrown an exception");
+ } catch (JSONException e) {
+ // no-op on purpose
+ }
+ }
+
+ private void doJsonWork() throws JSONException {
+ mDatabase.runInTransaction(() -> {
+ JSONObject json = new JSONObject();
+ return json.getString("key"); // method declares that it throws JSONException
+ });
+ }
+}
diff --git a/room/ktx/api/2.1.0-alpha06.txt b/room/ktx/api/2.1.0-alpha06.txt
index f5dcd16..851eb86 100644
--- a/room/ktx/api/2.1.0-alpha06.txt
+++ b/room/ktx/api/2.1.0-alpha06.txt
@@ -1,6 +1,10 @@
// Signature format: 3.0
package androidx.room {
+ public final class CoroutinesRoomKt {
+ ctor public CoroutinesRoomKt();
+ }
+
public final class RoomDatabaseKt {
ctor public RoomDatabaseKt();
method public static suspend Object? acquireTransactionThread(java.util.concurrent.Executor, kotlinx.coroutines.Job controlJob, kotlin.coroutines.experimental.Continuation<? super kotlin.coroutines.ContinuationInterceptor> p);
diff --git a/room/ktx/api/current.txt b/room/ktx/api/current.txt
index f5dcd16..851eb86 100644
--- a/room/ktx/api/current.txt
+++ b/room/ktx/api/current.txt
@@ -1,6 +1,10 @@
// Signature format: 3.0
package androidx.room {
+ public final class CoroutinesRoomKt {
+ ctor public CoroutinesRoomKt();
+ }
+
public final class RoomDatabaseKt {
ctor public RoomDatabaseKt();
method public static suspend Object? acquireTransactionThread(java.util.concurrent.Executor, kotlinx.coroutines.Job controlJob, kotlin.coroutines.experimental.Continuation<? super kotlin.coroutines.ContinuationInterceptor> p);
diff --git a/room/ktx/api/restricted_2.1.0-alpha06.txt b/room/ktx/api/restricted_2.1.0-alpha06.txt
index f31bf4d..866bb4d 100644
--- a/room/ktx/api/restricted_2.1.0-alpha06.txt
+++ b/room/ktx/api/restricted_2.1.0-alpha06.txt
@@ -2,12 +2,12 @@
package androidx.room {
@RestrictTo({RestrictTo.Scope.LIBRARY_GROUP_PREFIX}) public final class CoroutinesRoom {
- method public static suspend <R> Object? execute(androidx.room.RoomDatabase p, java.util.concurrent.Callable<R> db, kotlin.coroutines.experimental.Continuation<? super R> callable);
+ method public static suspend <R> Object? execute(androidx.room.RoomDatabase p, boolean db, java.util.concurrent.Callable<R> inTransaction, kotlin.coroutines.experimental.Continuation<? super R> callable);
field public static final androidx.room.CoroutinesRoom.Companion! Companion;
}
public static final class CoroutinesRoom.Companion {
- method public suspend <R> Object? execute(androidx.room.RoomDatabase db, java.util.concurrent.Callable<R> callable, kotlin.coroutines.experimental.Continuation<? super R> p);
+ method public suspend <R> Object? execute(androidx.room.RoomDatabase db, boolean inTransaction, java.util.concurrent.Callable<R> callable, kotlin.coroutines.experimental.Continuation<? super R> p);
}
}
diff --git a/room/ktx/api/restricted_current.txt b/room/ktx/api/restricted_current.txt
index f31bf4d..866bb4d 100644
--- a/room/ktx/api/restricted_current.txt
+++ b/room/ktx/api/restricted_current.txt
@@ -2,12 +2,12 @@
package androidx.room {
@RestrictTo({RestrictTo.Scope.LIBRARY_GROUP_PREFIX}) public final class CoroutinesRoom {
- method public static suspend <R> Object? execute(androidx.room.RoomDatabase p, java.util.concurrent.Callable<R> db, kotlin.coroutines.experimental.Continuation<? super R> callable);
+ method public static suspend <R> Object? execute(androidx.room.RoomDatabase p, boolean db, java.util.concurrent.Callable<R> inTransaction, kotlin.coroutines.experimental.Continuation<? super R> callable);
field public static final androidx.room.CoroutinesRoom.Companion! Companion;
}
public static final class CoroutinesRoom.Companion {
- method public suspend <R> Object? execute(androidx.room.RoomDatabase db, java.util.concurrent.Callable<R> callable, kotlin.coroutines.experimental.Continuation<? super R> p);
+ method public suspend <R> Object? execute(androidx.room.RoomDatabase db, boolean inTransaction, java.util.concurrent.Callable<R> callable, kotlin.coroutines.experimental.Continuation<? super R> p);
}
}
diff --git a/room/ktx/src/main/java/androidx/room/CoroutinesRoom.kt b/room/ktx/src/main/java/androidx/room/CoroutinesRoom.kt
index f4e3cc9..d38ed58 100644
--- a/room/ktx/src/main/java/androidx/room/CoroutinesRoom.kt
+++ b/room/ktx/src/main/java/androidx/room/CoroutinesRoom.kt
@@ -17,6 +17,7 @@
package androidx.room
import androidx.annotation.RestrictTo
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.withContext
import java.util.concurrent.Callable
@@ -33,18 +34,42 @@
companion object {
@JvmStatic
- suspend fun <R> execute(db: RoomDatabase, callable: Callable<R>): R {
+ suspend fun <R> execute(
+ db: RoomDatabase,
+ inTransaction: Boolean,
+ callable: Callable<R>
+ ): R {
if (db.isOpen && db.inTransaction()) {
return callable.call()
}
// Use the transaction dispatcher if we are on a transaction coroutine, otherwise
- // use the query executor as dispatcher.
+ // use the database dispatchers.
val context = coroutineContext[TransactionElement]?.transactionDispatcher
- ?: db.queryExecutor.asCoroutineDispatcher()
+ ?: if (inTransaction) db.transactionDispatcher else db.queryDispatcher
return withContext(context) {
callable.call()
}
}
}
-}
\ No newline at end of file
+}
+
+/**
+ * Gets the query coroutine dispatcher.
+ *
+ * @hide
+ */
+internal val RoomDatabase.queryDispatcher: CoroutineDispatcher
+ get() = backingFieldMap.getOrPut("QueryDispatcher") {
+ queryExecutor.asCoroutineDispatcher()
+ } as CoroutineDispatcher
+
+/**
+ * Gets the transaction coroutine dispatcher.
+ *
+ * @hide
+ */
+internal val RoomDatabase.transactionDispatcher: CoroutineDispatcher
+ get() = backingFieldMap.getOrPut("TransactionDispatcher") {
+ queryExecutor.asCoroutineDispatcher()
+ } as CoroutineDispatcher
diff --git a/room/ktx/src/main/java/androidx/room/RoomDatabase.kt b/room/ktx/src/main/java/androidx/room/RoomDatabase.kt
index 86f1fde..49a972a 100644
--- a/room/ktx/src/main/java/androidx/room/RoomDatabase.kt
+++ b/room/ktx/src/main/java/androidx/room/RoomDatabase.kt
@@ -91,7 +91,7 @@
*/
private suspend fun RoomDatabase.createTransactionContext(): CoroutineContext {
val controlJob = Job()
- val dispatcher = queryExecutor.acquireTransactionThread(controlJob)
+ val dispatcher = transactionExecutor.acquireTransactionThread(controlJob)
val transactionElement = TransactionElement(controlJob, dispatcher)
val threadLocalElement =
suspendingTransactionId.asContextElement(System.identityHashCode(controlJob))
diff --git a/room/runtime/api/2.1.0-alpha06.txt b/room/runtime/api/2.1.0-alpha06.txt
index 00d448a..882b112 100644
--- a/room/runtime/api/2.1.0-alpha06.txt
+++ b/room/runtime/api/2.1.0-alpha06.txt
@@ -15,6 +15,7 @@
field public final java.util.concurrent.Executor queryExecutor;
field public final boolean requireMigration;
field public final androidx.sqlite.db.SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory;
+ field public final java.util.concurrent.Executor transactionExecutor;
}
public class InvalidationTracker {
@@ -48,6 +49,7 @@
method public androidx.room.InvalidationTracker getInvalidationTracker();
method public androidx.sqlite.db.SupportSQLiteOpenHelper getOpenHelper();
method public java.util.concurrent.Executor getQueryExecutor();
+ method public java.util.concurrent.Executor getTransactionExecutor();
method public boolean inTransaction();
method @CallSuper public void init(androidx.room.DatabaseConfiguration);
method protected void internalInitInvalidationTracker(androidx.sqlite.db.SupportSQLiteDatabase);
@@ -73,6 +75,7 @@
method public androidx.room.RoomDatabase.Builder<T> openHelperFactory(androidx.sqlite.db.SupportSQLiteOpenHelper.Factory?);
method public androidx.room.RoomDatabase.Builder<T> setJournalMode(androidx.room.RoomDatabase.JournalMode);
method public androidx.room.RoomDatabase.Builder<T> setQueryExecutor(java.util.concurrent.Executor);
+ method public androidx.room.RoomDatabase.Builder<T> setTransactionExecutor(java.util.concurrent.Executor);
}
public abstract static class RoomDatabase.Callback {
diff --git a/room/runtime/api/current.txt b/room/runtime/api/current.txt
index 00d448a..882b112 100644
--- a/room/runtime/api/current.txt
+++ b/room/runtime/api/current.txt
@@ -15,6 +15,7 @@
field public final java.util.concurrent.Executor queryExecutor;
field public final boolean requireMigration;
field public final androidx.sqlite.db.SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory;
+ field public final java.util.concurrent.Executor transactionExecutor;
}
public class InvalidationTracker {
@@ -48,6 +49,7 @@
method public androidx.room.InvalidationTracker getInvalidationTracker();
method public androidx.sqlite.db.SupportSQLiteOpenHelper getOpenHelper();
method public java.util.concurrent.Executor getQueryExecutor();
+ method public java.util.concurrent.Executor getTransactionExecutor();
method public boolean inTransaction();
method @CallSuper public void init(androidx.room.DatabaseConfiguration);
method protected void internalInitInvalidationTracker(androidx.sqlite.db.SupportSQLiteDatabase);
@@ -73,6 +75,7 @@
method public androidx.room.RoomDatabase.Builder<T> openHelperFactory(androidx.sqlite.db.SupportSQLiteOpenHelper.Factory?);
method public androidx.room.RoomDatabase.Builder<T> setJournalMode(androidx.room.RoomDatabase.JournalMode);
method public androidx.room.RoomDatabase.Builder<T> setQueryExecutor(java.util.concurrent.Executor);
+ method public androidx.room.RoomDatabase.Builder<T> setTransactionExecutor(java.util.concurrent.Executor);
}
public abstract static class RoomDatabase.Callback {
diff --git a/room/runtime/api/restricted_2.1.0-alpha06.txt b/room/runtime/api/restricted_2.1.0-alpha06.txt
index 10b8084..a792dc1 100644
--- a/room/runtime/api/restricted_2.1.0-alpha06.txt
+++ b/room/runtime/api/restricted_2.1.0-alpha06.txt
@@ -2,7 +2,7 @@
package androidx.room {
public class DatabaseConfiguration {
- ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public DatabaseConfiguration(android.content.Context, String?, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory, androidx.room.RoomDatabase.MigrationContainer, java.util.List<androidx.room.RoomDatabase.Callback>?, boolean, androidx.room.RoomDatabase.JournalMode!, java.util.concurrent.Executor, boolean, boolean, boolean, java.util.Set<java.lang.Integer>?);
+ ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public DatabaseConfiguration(android.content.Context, String?, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory, androidx.room.RoomDatabase.MigrationContainer, java.util.List<androidx.room.RoomDatabase.Callback>?, boolean, androidx.room.RoomDatabase.JournalMode!, java.util.concurrent.Executor, java.util.concurrent.Executor, boolean, boolean, boolean, java.util.Set<java.lang.Integer>?);
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract class EntityDeletionOrUpdateAdapter<T> extends androidx.room.SharedSQLiteStatement {
@@ -32,7 +32,8 @@
ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public InvalidationTracker(androidx.room.RoomDatabase!, java.lang.String...!);
ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public InvalidationTracker(androidx.room.RoomDatabase!, java.util.Map<java.lang.String,java.lang.String>!, java.util.Map<java.lang.String,java.util.Set<java.lang.String>>!, java.lang.String...!);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void addWeakObserver(androidx.room.InvalidationTracker.Observer!);
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public <T> androidx.lifecycle.LiveData<T>! createLiveData(String[]!, java.util.concurrent.Callable<T>!);
+ method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public <T> androidx.lifecycle.LiveData<T>! createLiveData(String[]!, java.util.concurrent.Callable<T>!);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public <T> androidx.lifecycle.LiveData<T>! createLiveData(String[]!, boolean, java.util.concurrent.Callable<T>!);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @WorkerThread public void refreshVersionsSync();
}
diff --git a/room/runtime/api/restricted_current.txt b/room/runtime/api/restricted_current.txt
index 10b8084..a792dc1 100644
--- a/room/runtime/api/restricted_current.txt
+++ b/room/runtime/api/restricted_current.txt
@@ -2,7 +2,7 @@
package androidx.room {
public class DatabaseConfiguration {
- ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public DatabaseConfiguration(android.content.Context, String?, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory, androidx.room.RoomDatabase.MigrationContainer, java.util.List<androidx.room.RoomDatabase.Callback>?, boolean, androidx.room.RoomDatabase.JournalMode!, java.util.concurrent.Executor, boolean, boolean, boolean, java.util.Set<java.lang.Integer>?);
+ ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public DatabaseConfiguration(android.content.Context, String?, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory, androidx.room.RoomDatabase.MigrationContainer, java.util.List<androidx.room.RoomDatabase.Callback>?, boolean, androidx.room.RoomDatabase.JournalMode!, java.util.concurrent.Executor, java.util.concurrent.Executor, boolean, boolean, boolean, java.util.Set<java.lang.Integer>?);
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract class EntityDeletionOrUpdateAdapter<T> extends androidx.room.SharedSQLiteStatement {
@@ -32,7 +32,8 @@
ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public InvalidationTracker(androidx.room.RoomDatabase!, java.lang.String...!);
ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public InvalidationTracker(androidx.room.RoomDatabase!, java.util.Map<java.lang.String,java.lang.String>!, java.util.Map<java.lang.String,java.util.Set<java.lang.String>>!, java.lang.String...!);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void addWeakObserver(androidx.room.InvalidationTracker.Observer!);
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public <T> androidx.lifecycle.LiveData<T>! createLiveData(String[]!, java.util.concurrent.Callable<T>!);
+ method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public <T> androidx.lifecycle.LiveData<T>! createLiveData(String[]!, java.util.concurrent.Callable<T>!);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public <T> androidx.lifecycle.LiveData<T>! createLiveData(String[]!, boolean, java.util.concurrent.Callable<T>!);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @WorkerThread public void refreshVersionsSync();
}
diff --git a/room/runtime/build.gradle b/room/runtime/build.gradle
index c7e7958..634aea6 100644
--- a/room/runtime/build.gradle
+++ b/room/runtime/build.gradle
@@ -32,6 +32,10 @@
dependencies {
api(project(":room:room-common"))
+ implementation fileTree(
+ dir: "${new File(project(":room:room-common-java8").buildDir, "libs")}",
+ include : "*.jar"
+ )
api(ANDROIDX_SQLITE_FRAMEWORK)
api(ANDROIDX_SQLITE)
implementation(ARCH_CORE_RUNTIME)
@@ -46,6 +50,7 @@
testImplementation(MOCKITO_CORE)
testImplementation(ARCH_LIFECYCLE_EXTENSIONS)
testImplementation(KOTLIN_STDLIB)
+ testImplementation(TRUTH)
androidTestImplementation(JUNIT)
androidTestImplementation(TEST_EXT_JUNIT)
@@ -56,15 +61,21 @@
androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
}
-// Used by testCompile in room-compiler
android.libraryVariants.all { variant ->
def name = variant.name
def suffix = name.capitalize()
+
+ // Create jar<variant> task for testCompile in room-compiler.
project.tasks.create(name: "jar${suffix}", type: Jar){
dependsOn variant.javaCompileProvider.get()
from variant.javaCompileProvider.get().destinationDir
destinationDir new File(project.buildDir, "libJar")
}
+
+ // Make javaCompile task depend on room-common-java8 jar task.
+ variant.javaCompileProvider.configure { task ->
+ task.dependsOn(":room:room-common-java8:jar")
+ }
}
supportLibrary {
diff --git a/room/runtime/src/main/java/androidx/room/DatabaseConfiguration.java b/room/runtime/src/main/java/androidx/room/DatabaseConfiguration.java
index bb893fd..dae3a09 100644
--- a/room/runtime/src/main/java/androidx/room/DatabaseConfiguration.java
+++ b/room/runtime/src/main/java/androidx/room/DatabaseConfiguration.java
@@ -74,6 +74,12 @@
public final Executor queryExecutor;
/**
+ * The Executor used to execute asynchronous transactions.
+ */
+ @NonNull
+ public final Executor transactionExecutor;
+
+ /**
* If true, table invalidation in an instance of {@link RoomDatabase} is broadcast and
* synchronized with other instances of the same {@link RoomDatabase} file, including those
* in a separate process.
@@ -124,6 +130,7 @@
boolean allowMainThreadQueries,
RoomDatabase.JournalMode journalMode,
@NonNull Executor queryExecutor,
+ @NonNull Executor transactionExecutor,
boolean multiInstanceInvalidation,
boolean requireMigration,
boolean allowDestructiveMigrationOnDowngrade,
@@ -136,6 +143,7 @@
this.allowMainThreadQueries = allowMainThreadQueries;
this.journalMode = journalMode;
this.queryExecutor = queryExecutor;
+ this.transactionExecutor = transactionExecutor;
this.multiInstanceInvalidation = multiInstanceInvalidation;
this.requireMigration = requireMigration;
this.allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade;
diff --git a/room/runtime/src/main/java/androidx/room/InvalidationLiveDataContainer.java b/room/runtime/src/main/java/androidx/room/InvalidationLiveDataContainer.java
index 4168f47..e1e8155 100644
--- a/room/runtime/src/main/java/androidx/room/InvalidationLiveDataContainer.java
+++ b/room/runtime/src/main/java/androidx/room/InvalidationLiveDataContainer.java
@@ -43,8 +43,10 @@
mDatabase = database;
}
- <T> LiveData<T> create(String[] tableNames, Callable<T> computeFunction) {
- return new RoomTrackingLiveData<>(mDatabase, this, computeFunction, tableNames);
+ <T> LiveData<T> create(String[] tableNames, boolean inTransaction,
+ Callable<T> computeFunction) {
+ return new RoomTrackingLiveData<>(mDatabase, this, inTransaction, computeFunction,
+ tableNames);
}
void onActive(LiveData liveData) {
diff --git a/room/runtime/src/main/java/androidx/room/InvalidationTracker.java b/room/runtime/src/main/java/androidx/room/InvalidationTracker.java
index 4d3161a..9e7be31 100644
--- a/room/runtime/src/main/java/androidx/room/InvalidationTracker.java
+++ b/room/runtime/src/main/java/androidx/room/InvalidationTracker.java
@@ -559,6 +559,8 @@
* <p>
* Holds a strong reference to the created LiveData as long as it is active.
*
+ * @deprecated Use {@link #createLiveData(String[], boolean, Callable)}
+ *
* @param computeFunction The function that calculates the value
* @param tableNames The list of tables to observe
* @param <T> The return type
@@ -566,10 +568,32 @@
* invalidates.
* @hide
*/
+ @Deprecated
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public <T> LiveData<T> createLiveData(String[] tableNames, Callable<T> computeFunction) {
+ return createLiveData(tableNames, false, computeFunction);
+ }
+
+ /**
+ * Creates a LiveData that computes the given function once and for every other invalidation
+ * of the database.
+ * <p>
+ * Holds a strong reference to the created LiveData as long as it is active.
+ *
+ * @param tableNames The list of tables to observe
+ * @param inTransaction True if the computeFunction will be done in a transaction, false
+ * otherwise.
+ * @param computeFunction The function that calculates the value
+ * @param <T> The return type
+ * @return A new LiveData that computes the given function when the given list of tables
+ * invalidates.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public <T> LiveData<T> createLiveData(String[] tableNames, boolean inTransaction,
+ Callable<T> computeFunction) {
return mInvalidationLiveDataContainer.create(
- validateAndResolveTableNames(tableNames), computeFunction);
+ validateAndResolveTableNames(tableNames), inTransaction, computeFunction);
}
/**
diff --git a/room/runtime/src/main/java/androidx/room/RoomDatabase.java b/room/runtime/src/main/java/androidx/room/RoomDatabase.java
index a33383a..d89ac58 100644
--- a/room/runtime/src/main/java/androidx/room/RoomDatabase.java
+++ b/room/runtime/src/main/java/androidx/room/RoomDatabase.java
@@ -34,6 +34,7 @@
import androidx.collection.SparseArrayCompat;
import androidx.core.app.ActivityManagerCompat;
import androidx.room.migration.Migration;
+import androidx.room.util.SneakyThrow;
import androidx.sqlite.db.SimpleSQLiteQuery;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
@@ -45,8 +46,10 @@
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@@ -77,6 +80,7 @@
@Deprecated
protected volatile SupportSQLiteDatabase mDatabase;
private Executor mQueryExecutor;
+ private Executor mTransactionExecutor;
private SupportSQLiteOpenHelper mOpenHelper;
private final InvalidationTracker mInvalidationTracker;
private boolean mAllowMainThreadQueries;
@@ -121,6 +125,19 @@
return mSuspendingTransactionId;
}
+
+ private final Map<String, Object> mBackingFieldMap = new ConcurrentHashMap<>();
+
+ /**
+ * Gets the map for storing extension properties of Kotlin type.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ Map<String, Object> getBackingFieldMap() {
+ return mBackingFieldMap;
+ }
+
/**
* Creates a RoomDatabase.
* <p>
@@ -147,6 +164,7 @@
}
mCallbacks = configuration.callbacks;
mQueryExecutor = configuration.queryExecutor;
+ mTransactionExecutor = new TransactionExecutor(configuration.transactionExecutor);
mAllowMainThreadQueries = configuration.allowMainThreadQueries;
mWriteAheadLoggingEnabled = wal;
if (configuration.multiInstanceInvalidation) {
@@ -336,6 +354,14 @@
}
/**
+ * @return The Executor in use by this database for async transactions.
+ */
+ @NonNull
+ public Executor getTransactionExecutor() {
+ return mTransactionExecutor;
+ }
+
+ /**
* Wrapper for {@link SupportSQLiteDatabase#setTransactionSuccessful()}.
*
* @deprecated Use {@link #runInTransaction(Runnable)}
@@ -380,7 +406,8 @@
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
- throw new RuntimeException("Exception in transaction", e);
+ SneakyThrow.reThrow(e);
+ return null; // Unreachable code, but compiler doesn't know it.
} finally {
endTransaction();
}
@@ -481,6 +508,8 @@
/** The Executor used to run database queries. This should be background-threaded. */
private Executor mQueryExecutor;
+ /** The Executor used to run database transactions. This should be background-threaded. */
+ private Executor mTransactionExecutor;
private SupportSQLiteOpenHelper.Factory mFactory;
private boolean mAllowMainThreadQueries;
private JournalMode mJournalMode;
@@ -598,12 +627,19 @@
* queries and tasks, including {@code LiveData} invalidation, {@code Flowable} scheduling
* and {@code ListenableFuture} tasks.
* <p>
- * When unset, a default {@code Executor} will be used. The default {@code Executor}
- * allocates and shares threads amongst Architecture Components libraries.
+ * When both the query executor and transaction executor are unset, then a default
+ * {@code Executor} will be used. The default {@code Executor} allocates and shares threads
+ * amongst Architecture Components libraries. If the query executor is unset but a
+ * transaction executor was set, then the same {@code Executor} will be used for queries.
+ * <p>
+ * For best performance the given {@code Executor} should be bounded (max number of threads
+ * is limited).
* <p>
* The input {@code Executor} cannot run tasks on the UI thread.
- *
+ **
* @return this
+ *
+ * @see #setTransactionExecutor(Executor)
*/
@NonNull
public Builder<T> setQueryExecutor(@NonNull Executor executor) {
@@ -612,6 +648,32 @@
}
/**
+ * Sets the {@link Executor} that will be used to execute all non-blocking asynchronous
+ * transaction queries and tasks, including {@code LiveData} invalidation, {@code Flowable}
+ * scheduling and {@code ListenableFuture} tasks.
+ * <p>
+ * When both the transaction executor and query executor are unset, then a default
+ * {@code Executor} will be used. The default {@code Executor} allocates and shares threads
+ * amongst Architecture Components libraries. If the transaction executor is unset but a
+ * query executor was set, then the same {@code Executor} will be used for transactions.
+ * <p>
+ * If the given {@code Executor} is shared then it should be unbounded to avoid the
+ * possibility of a deadlock. Room will not use more than one thread at a time from this
+ * executor.
+ * <p>
+ * The input {@code Executor} cannot run tasks on the UI thread.
+ *
+ * @return this
+ *
+ * @see #setQueryExecutor(Executor)
+ */
+ @NonNull
+ public Builder<T> setTransactionExecutor(@NonNull Executor executor) {
+ mTransactionExecutor = executor;
+ return this;
+ }
+
+ /**
* Sets whether table invalidation in this instance of {@link RoomDatabase} should be
* broadcast and synchronized with other instances of the same {@link RoomDatabase},
* including those in a separate process. In order to enable multi-instance invalidation,
@@ -741,8 +803,12 @@
throw new IllegalArgumentException("Must provide an abstract class that"
+ " extends RoomDatabase");
}
- if (mQueryExecutor == null) {
- mQueryExecutor = ArchTaskExecutor.getIOThreadExecutor();
+ if (mQueryExecutor == null && mTransactionExecutor == null) {
+ mQueryExecutor = mTransactionExecutor = ArchTaskExecutor.getIOThreadExecutor();
+ } else if (mQueryExecutor != null && mTransactionExecutor == null) {
+ mTransactionExecutor = mQueryExecutor;
+ } else if (mQueryExecutor == null && mTransactionExecutor != null) {
+ mQueryExecutor = mTransactionExecutor;
}
if (mMigrationStartAndEndVersions != null && mMigrationsNotRequiredFrom != null) {
@@ -763,12 +829,20 @@
mFactory = new FrameworkSQLiteOpenHelperFactory();
}
DatabaseConfiguration configuration =
- new DatabaseConfiguration(mContext, mName, mFactory, mMigrationContainer,
- mCallbacks, mAllowMainThreadQueries, mJournalMode.resolve(mContext),
+ new DatabaseConfiguration(
+ mContext,
+ mName,
+ mFactory,
+ mMigrationContainer,
+ mCallbacks,
+ mAllowMainThreadQueries,
+ mJournalMode.resolve(mContext),
mQueryExecutor,
+ mTransactionExecutor,
mMultiInstanceInvalidation,
mRequireMigration,
- mAllowDestructiveMigrationOnDowngrade, mMigrationsNotRequiredFrom);
+ mAllowDestructiveMigrationOnDowngrade,
+ mMigrationsNotRequiredFrom);
T db = Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX);
db.init(configuration);
return db;
diff --git a/room/runtime/src/main/java/androidx/room/RoomTrackingLiveData.java b/room/runtime/src/main/java/androidx/room/RoomTrackingLiveData.java
index 61ef38e..8df1014 100644
--- a/room/runtime/src/main/java/androidx/room/RoomTrackingLiveData.java
+++ b/room/runtime/src/main/java/androidx/room/RoomTrackingLiveData.java
@@ -27,6 +27,7 @@
import java.util.Set;
import java.util.concurrent.Callable;
+import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
/**
@@ -48,6 +49,9 @@
final RoomDatabase mDatabase;
@SuppressWarnings("WeakerAccess")
+ final boolean mInTransaction;
+
+ @SuppressWarnings("WeakerAccess")
final Callable<T> mComputeFunction;
private final InvalidationLiveDataContainer mContainer;
@@ -116,7 +120,7 @@
boolean isActive = hasActiveObservers();
if (mInvalid.compareAndSet(false, true)) {
if (isActive) {
- mDatabase.getQueryExecutor().execute(mRefreshRunnable);
+ getQueryExecutor().execute(mRefreshRunnable);
}
}
}
@@ -125,9 +129,11 @@
RoomTrackingLiveData(
RoomDatabase database,
InvalidationLiveDataContainer container,
+ boolean inTransaction,
Callable<T> computeFunction,
String[] tableNames) {
mDatabase = database;
+ mInTransaction = inTransaction;
mComputeFunction = computeFunction;
mContainer = container;
mObserver = new InvalidationTracker.Observer(tableNames) {
@@ -142,7 +148,7 @@
protected void onActive() {
super.onActive();
mContainer.onActive(this);
- mDatabase.getQueryExecutor().execute(mRefreshRunnable);
+ getQueryExecutor().execute(mRefreshRunnable);
}
@Override
@@ -150,4 +156,12 @@
super.onInactive();
mContainer.onInactive(this);
}
+
+ Executor getQueryExecutor() {
+ if (mInTransaction) {
+ return mDatabase.getTransactionExecutor();
+ } else {
+ return mDatabase.getQueryExecutor();
+ }
+ }
}
diff --git a/room/runtime/src/main/java/androidx/room/TransactionExecutor.java b/room/runtime/src/main/java/androidx/room/TransactionExecutor.java
new file mode 100644
index 0000000..6a6bc12
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/TransactionExecutor.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2019 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 androidx.room;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayDeque;
+import java.util.concurrent.Executor;
+
+/**
+ * Executor wrapper for performing database transactions serially.
+ * <p>
+ * Since database transactions are exclusive, this executor ensures that transactions are performed
+ * in-order and one at a time, preventing threads from blocking each other when multiple concurrent
+ * transactions are attempted.
+ */
+class TransactionExecutor implements Executor {
+
+ private final Executor mExecutor;
+ private final ArrayDeque<Runnable> mTasks = new ArrayDeque<>();
+ private Runnable mActive;
+
+ TransactionExecutor(@NonNull Executor executor) {
+ mExecutor = executor;
+ }
+
+ public synchronized void execute(final Runnable command) {
+ mTasks.offer(new Runnable() {
+ public void run() {
+ try {
+ command.run();
+ } finally {
+ scheduleNext();
+ }
+ }
+ });
+ if (mActive == null) {
+ scheduleNext();
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ synchronized void scheduleNext() {
+ if ((mActive = mTasks.poll()) != null) {
+ mExecutor.execute(mActive);
+ }
+ }
+}
diff --git a/room/runtime/src/main/java/androidx/room/paging/LimitOffsetDataSource.java b/room/runtime/src/main/java/androidx/room/paging/LimitOffsetDataSource.java
index 0607dd5..845b64e 100644
--- a/room/runtime/src/main/java/androidx/room/paging/LimitOffsetDataSource.java
+++ b/room/runtime/src/main/java/androidx/room/paging/LimitOffsetDataSource.java
@@ -113,7 +113,7 @@
int firstLoadPosition = 0;
RoomSQLiteQuery sqLiteQuery = null;
Cursor cursor = null;
-
+ //noinspection deprecation
mDb.beginTransaction();
try {
totalCount = countItems();
@@ -125,6 +125,7 @@
sqLiteQuery = getSQLiteQuery(firstLoadPosition, firstLoadSize);
cursor = mDb.query(sqLiteQuery);
List<T> rows = convertRows(cursor);
+ //noinspection deprecation
mDb.setTransactionSuccessful();
list = rows;
}
@@ -132,6 +133,7 @@
if (cursor != null) {
cursor.close();
}
+ //noinspection deprecation
mDb.endTransaction();
if (sqLiteQuery != null) {
sqLiteQuery.release();
diff --git a/room/runtime/src/test/java/androidx/room/BuilderTest.java b/room/runtime/src/test/java/androidx/room/BuilderTest.java
index 0c29dd7..eabefdb 100644
--- a/room/runtime/src/test/java/androidx/room/BuilderTest.java
+++ b/room/runtime/src/test/java/androidx/room/BuilderTest.java
@@ -39,6 +39,7 @@
import org.junit.runners.JUnit4;
import java.util.List;
+import java.util.concurrent.Executor;
@SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
@RunWith(JUnit4.class)
@@ -66,6 +67,41 @@
Room.databaseBuilder(mock(Context.class), RoomDatabase.class, " ").build();
}
+ public void executors_setQueryExecutor() {
+ Executor executor = mock(Executor.class);
+
+ TestDatabase db = Room.databaseBuilder(mock(Context.class), TestDatabase.class, "foo")
+ .setQueryExecutor(executor)
+ .build();
+
+ assertThat(db.mDatabaseConfiguration.queryExecutor, is(executor));
+ assertThat(db.mDatabaseConfiguration.transactionExecutor, is(executor));
+ }
+
+ public void executors_setTransactionExecutor() {
+ Executor executor = mock(Executor.class);
+
+ TestDatabase db = Room.databaseBuilder(mock(Context.class), TestDatabase.class, "foo")
+ .setTransactionExecutor(executor)
+ .build();
+
+ assertThat(db.mDatabaseConfiguration.queryExecutor, is(executor));
+ assertThat(db.mDatabaseConfiguration.transactionExecutor, is(executor));
+ }
+
+ public void executors_setBothExecutors() {
+ Executor executor1 = mock(Executor.class);
+ Executor executor2 = mock(Executor.class);
+
+ TestDatabase db = Room.databaseBuilder(mock(Context.class), TestDatabase.class, "foo")
+ .setQueryExecutor(executor1)
+ .setTransactionExecutor(executor2)
+ .build();
+
+ assertThat(db.mDatabaseConfiguration.queryExecutor, is(executor1));
+ assertThat(db.mDatabaseConfiguration.transactionExecutor, is(executor2));
+ }
+
@Test
public void migration() {
Migration m1 = new EmptyMigration(0, 1);
@@ -387,6 +423,14 @@
}
abstract static class TestDatabase extends RoomDatabase {
+
+ DatabaseConfiguration mDatabaseConfiguration;
+
+ @Override
+ public void init(@NonNull DatabaseConfiguration configuration) {
+ super.init(configuration);
+ mDatabaseConfiguration = configuration;
+ }
}
static class EmptyMigration extends Migration {
diff --git a/room/runtime/src/test/java/androidx/room/InvalidationLiveDataContainerTest.kt b/room/runtime/src/test/java/androidx/room/InvalidationLiveDataContainerTest.kt
index 97c22d1..6034b67 100644
--- a/room/runtime/src/test/java/androidx/room/InvalidationLiveDataContainerTest.kt
+++ b/room/runtime/src/test/java/androidx/room/InvalidationLiveDataContainerTest.kt
@@ -99,6 +99,7 @@
private fun createLiveData(): LiveData<Any> {
return container.create(
arrayOf("a", "b"),
+ false,
createComputeFunction<Any>()
) as LiveData
}
diff --git a/room/runtime/src/test/java/androidx/room/TransactionExecutorTest.kt b/room/runtime/src/test/java/androidx/room/TransactionExecutorTest.kt
new file mode 100644
index 0000000..edda059
--- /dev/null
+++ b/room/runtime/src/test/java/androidx/room/TransactionExecutorTest.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2019 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 androidx.room
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+
+@RunWith(JUnit4::class)
+class TransactionExecutorTest {
+
+ private val testExecutor = Executors.newCachedThreadPool()
+ private val transactionExecutor = TransactionExecutor(testExecutor)
+
+ @After
+ fun teardown() {
+ testExecutor.shutdownNow()
+ }
+
+ @Test
+ @Throws(InterruptedException::class)
+ fun testSerialExecution() {
+
+ val latch = CountDownLatch(3)
+ val runnableA = TimingRunnable(latch)
+ val runnableB = TimingRunnable(latch)
+ val runnableC = TimingRunnable(latch)
+
+ transactionExecutor.execute(runnableA)
+ transactionExecutor.execute(runnableB)
+ transactionExecutor.execute(runnableC)
+
+ // Await for the runnables to finish.
+ latch.await(1, TimeUnit.SECONDS)
+
+ // Assert that everything ran.
+ assertThat(runnableA.run).isTrue()
+ assertThat(runnableB.run).isTrue()
+ assertThat(runnableC.run).isTrue()
+
+ // Assert that runnables were run in order of submission.
+ assertThat(runnableA.start).isLessThan(runnableB.start)
+ assertThat(runnableB.start).isLessThan(runnableC.start)
+
+ // Assert that a runnable finishes before the runnable after it starts.
+ assertThat(runnableA.finish).isLessThan(runnableB.start)
+ assertThat(runnableB.finish).isLessThan(runnableC.start)
+ }
+
+ private class TimingRunnable(val latch: CountDownLatch) : Runnable {
+ var start: Long = 0
+ var finish: Long = 0
+ var run: Boolean = false
+
+ override fun run() {
+ run = true
+ start = System.nanoTime()
+ try {
+ // Sleep for a bit as if we were doing real work.
+ Thread.sleep(100)
+ } catch (e: InterruptedException) {
+ throw RuntimeException(e)
+ }
+ finish = System.nanoTime()
+ latch.countDown()
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/rxjava2/api/restricted_2.1.0-alpha06.txt b/room/rxjava2/api/restricted_2.1.0-alpha06.txt
index 36290b8..6731dfa 100644
--- a/room/rxjava2/api/restricted_2.1.0-alpha06.txt
+++ b/room/rxjava2/api/restricted_2.1.0-alpha06.txt
@@ -2,8 +2,10 @@
package androidx.room {
public class RxRoom {
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <T> io.reactivex.Flowable<T>! createFlowable(androidx.room.RoomDatabase!, String[]!, java.util.concurrent.Callable<T>!);
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <T> io.reactivex.Observable<T>! createObservable(androidx.room.RoomDatabase!, String[]!, java.util.concurrent.Callable<T>!);
+ method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <T> io.reactivex.Flowable<T>! createFlowable(androidx.room.RoomDatabase!, String[]!, java.util.concurrent.Callable<T>!);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <T> io.reactivex.Flowable<T>! createFlowable(androidx.room.RoomDatabase!, boolean, String[]!, java.util.concurrent.Callable<T>!);
+ method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <T> io.reactivex.Observable<T>! createObservable(androidx.room.RoomDatabase!, String[]!, java.util.concurrent.Callable<T>!);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <T> io.reactivex.Observable<T>! createObservable(androidx.room.RoomDatabase!, boolean, String[]!, java.util.concurrent.Callable<T>!);
}
}
diff --git a/room/rxjava2/api/restricted_current.txt b/room/rxjava2/api/restricted_current.txt
index 36290b8..6731dfa 100644
--- a/room/rxjava2/api/restricted_current.txt
+++ b/room/rxjava2/api/restricted_current.txt
@@ -2,8 +2,10 @@
package androidx.room {
public class RxRoom {
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <T> io.reactivex.Flowable<T>! createFlowable(androidx.room.RoomDatabase!, String[]!, java.util.concurrent.Callable<T>!);
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <T> io.reactivex.Observable<T>! createObservable(androidx.room.RoomDatabase!, String[]!, java.util.concurrent.Callable<T>!);
+ method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <T> io.reactivex.Flowable<T>! createFlowable(androidx.room.RoomDatabase!, String[]!, java.util.concurrent.Callable<T>!);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <T> io.reactivex.Flowable<T>! createFlowable(androidx.room.RoomDatabase!, boolean, String[]!, java.util.concurrent.Callable<T>!);
+ method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <T> io.reactivex.Observable<T>! createObservable(androidx.room.RoomDatabase!, String[]!, java.util.concurrent.Callable<T>!);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <T> io.reactivex.Observable<T>! createObservable(androidx.room.RoomDatabase!, boolean, String[]!, java.util.concurrent.Callable<T>!);
}
}
diff --git a/room/rxjava2/src/main/java/androidx/room/RxRoom.java b/room/rxjava2/src/main/java/androidx/room/RxRoom.java
index 1e78572..3badde1 100644
--- a/room/rxjava2/src/main/java/androidx/room/RxRoom.java
+++ b/room/rxjava2/src/main/java/androidx/room/RxRoom.java
@@ -20,6 +20,7 @@
import java.util.Set;
import java.util.concurrent.Callable;
+import java.util.concurrent.Executor;
import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable;
@@ -97,12 +98,27 @@
* Helper method used by generated code to bind a Callable such that it will be run in
* our disk io thread and will automatically block null values since RxJava2 does not like null.
*
+ * @deprecated Use {@link #createFlowable(RoomDatabase, boolean, String[], Callable)}
+ *
+ * @hide
+ */
+ @Deprecated
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public static <T> Flowable<T> createFlowable(final RoomDatabase database,
+ final String[] tableNames, final Callable<T> callable) {
+ return createFlowable(database, false, tableNames, callable);
+ }
+
+ /**
+ * Helper method used by generated code to bind a Callable such that it will be run in
+ * our disk io thread and will automatically block null values since RxJava2 does not like null.
+ *
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public static <T> Flowable<T> createFlowable(final RoomDatabase database,
- final String[] tableNames, final Callable<T> callable) {
- Scheduler scheduler = Schedulers.from(database.getQueryExecutor());
+ final boolean inTransaction, final String[] tableNames, final Callable<T> callable) {
+ Scheduler scheduler = Schedulers.from(getExecutor(database, inTransaction));
final Maybe<T> maybe = Maybe.fromCallable(callable);
return createFlowable(database, tableNames)
.subscribeOn(scheduler)
@@ -161,12 +177,27 @@
* Helper method used by generated code to bind a Callable such that it will be run in
* our disk io thread and will automatically block null values since RxJava2 does not like null.
*
+ * @deprecated Use {@link #createObservable(RoomDatabase, boolean, String[], Callable)}
+ *
+ * @hide
+ */
+ @Deprecated
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public static <T> Observable<T> createObservable(final RoomDatabase database,
+ final String[] tableNames, final Callable<T> callable) {
+ return createObservable(database, false, tableNames, callable);
+ }
+
+ /**
+ * Helper method used by generated code to bind a Callable such that it will be run in
+ * our disk io thread and will automatically block null values since RxJava2 does not like null.
+ *
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public static <T> Observable<T> createObservable(final RoomDatabase database,
- final String[] tableNames, final Callable<T> callable) {
- Scheduler scheduler = Schedulers.from(database.getQueryExecutor());
+ final boolean inTransaction, final String[] tableNames, final Callable<T> callable) {
+ Scheduler scheduler = Schedulers.from(getExecutor(database, inTransaction));
final Maybe<T> maybe = Maybe.fromCallable(callable);
return createObservable(database, tableNames)
.subscribeOn(scheduler)
@@ -180,6 +211,14 @@
});
}
+ private static Executor getExecutor(RoomDatabase database, boolean inTransaction) {
+ if (inTransaction) {
+ return database.getTransactionExecutor();
+ } else {
+ return database.getQueryExecutor();
+ }
+ }
+
/** @deprecated This type should not be instantiated as it contains only static methods. */
@Deprecated
@SuppressWarnings("PrivateConstructorForUtilityClass")
diff --git a/room/rxjava2/src/test/java/androidx/room/RxRoomTest.java b/room/rxjava2/src/test/java/androidx/room/RxRoomTest.java
index cc96b50..dd5bebc 100644
--- a/room/rxjava2/src/test/java/androidx/room/RxRoomTest.java
+++ b/room/rxjava2/src/test/java/androidx/room/RxRoomTest.java
@@ -167,7 +167,7 @@
final AtomicReference<String> value = new AtomicReference<>(null);
String[] tables = {"a", "b"};
Set<String> tableSet = new HashSet<>(Arrays.asList(tables));
- final Flowable<String> flowable = RxRoom.createFlowable(mDatabase, tables,
+ final Flowable<String> flowable = RxRoom.createFlowable(mDatabase, false, tables,
new Callable<String>() {
@Override
public String call() throws Exception {
@@ -201,7 +201,7 @@
final AtomicReference<String> value = new AtomicReference<>(null);
String[] tables = {"a", "b"};
Set<String> tableSet = new HashSet<>(Arrays.asList(tables));
- final Observable<String> flowable = RxRoom.createObservable(mDatabase, tables,
+ final Observable<String> flowable = RxRoom.createObservable(mDatabase, false, tables,
new Callable<String>() {
@Override
public String call() throws Exception {
@@ -232,7 +232,7 @@
@Test
public void exception_Flowable() throws Exception {
- final Flowable<String> flowable = RxRoom.createFlowable(mDatabase, new String[]{"a"},
+ final Flowable<String> flowable = RxRoom.createFlowable(mDatabase, false, new String[]{"a"},
new Callable<String>() {
@Override
public String call() throws Exception {
@@ -248,7 +248,8 @@
@Test
public void exception_Observable() throws Exception {
- final Observable<String> flowable = RxRoom.createObservable(mDatabase, new String[]{"a"},
+ final Observable<String> flowable = RxRoom.createObservable(mDatabase, false,
+ new String[]{"a"},
new Callable<String>() {
@Override
public String call() throws Exception {
diff --git a/room/testing/src/main/java/androidx/room/testing/MigrationTestHelper.java b/room/testing/src/main/java/androidx/room/testing/MigrationTestHelper.java
index c0e1c4b..06b5a6b 100644
--- a/room/testing/src/main/java/androidx/room/testing/MigrationTestHelper.java
+++ b/room/testing/src/main/java/androidx/room/testing/MigrationTestHelper.java
@@ -158,6 +158,7 @@
true,
RoomDatabase.JournalMode.TRUNCATE,
ArchTaskExecutor.getIOThreadExecutor(),
+ ArchTaskExecutor.getIOThreadExecutor(),
false,
true,
false,
@@ -215,6 +216,7 @@
true,
RoomDatabase.JournalMode.TRUNCATE,
ArchTaskExecutor.getIOThreadExecutor(),
+ ArchTaskExecutor.getIOThreadExecutor(),
false,
true,
false,
diff --git a/samples/Support4Demos/src/main/AndroidManifest.xml b/samples/Support4Demos/src/main/AndroidManifest.xml
index 52d24be..295caf0 100644
--- a/samples/Support4Demos/src/main/AndroidManifest.xml
+++ b/samples/Support4Demos/src/main/AndroidManifest.xml
@@ -136,14 +136,6 @@
</intent-filter>
</activity>
- <activity android:name=".app.FragmentNestingTabsSupport"
- android:label="@string/fragment_nesting_tabs_support">
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="com.example.android.supportv4.SUPPORT4_SAMPLE_CODE" />
- </intent-filter>
- </activity>
-
<activity android:name=".app.FragmentRetainInstanceSupport"
android:label="@string/fragment_retain_instance_support">
<intent-filter>
@@ -168,22 +160,6 @@
</intent-filter>
</activity>
- <activity android:name=".app.FragmentTabs"
- android:label="@string/fragment_tabs">
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="com.example.android.supportv4.SUPPORT4_SAMPLE_CODE" />
- </intent-filter>
- </activity>
-
- <activity android:name=".app.FragmentTabsPager"
- android:label="@string/fragment_tabs_pager">
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="com.example.android.supportv4.SUPPORT4_SAMPLE_CODE" />
- </intent-filter>
- </activity>
-
<activity android:name=".app.FragmentPagerSupport"
android:label="@string/fragment_pager_support">
<intent-filter>
diff --git a/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentNestingTabsSupport.java b/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentNestingTabsSupport.java
index 0991eb8..61b37b7 100644
--- a/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentNestingTabsSupport.java
+++ b/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentNestingTabsSupport.java
@@ -16,33 +16,4 @@
package com.example.android.supportv4.app;
//BEGIN_INCLUDE(complete)
-
-import android.os.Bundle;
-
-import androidx.fragment.app.FragmentActivity;
-import androidx.fragment.app.FragmentTabHost;
-
-import com.example.android.supportv4.R;
-
-public class FragmentNestingTabsSupport extends FragmentActivity {
- private FragmentTabHost mTabHost;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- mTabHost = new FragmentTabHost(this);
- setContentView(mTabHost);
- mTabHost.setup(this, getSupportFragmentManager(), R.id.fragment1);
-
- mTabHost.addTab(mTabHost.newTabSpec("menus").setIndicator("Menus"),
- FragmentMenuFragmentSupport.class, null);
- mTabHost.addTab(mTabHost.newTabSpec("contacts").setIndicator("Contacts"),
- LoaderCursorSupport.CursorLoaderListFragment.class, null);
- mTabHost.addTab(mTabHost.newTabSpec("stack").setIndicator("Stack"),
- FragmentStackFragmentSupport.class, null);
- mTabHost.addTab(mTabHost.newTabSpec("tabs").setIndicator("Tabs"),
- FragmentTabsFragmentSupport.class, null);
- }
-}
//END_INCLUDE(complete)
diff --git a/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentTabs.java b/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentTabs.java
index c4bbd93..25cfea4 100644
--- a/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentTabs.java
+++ b/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentTabs.java
@@ -16,37 +16,4 @@
package com.example.android.supportv4.app;
//BEGIN_INCLUDE(complete)
-
-import android.os.Bundle;
-
-import androidx.fragment.app.FragmentActivity;
-import androidx.fragment.app.FragmentTabHost;
-
-import com.example.android.supportv4.R;
-
-/**
- * This demonstrates how you can implement switching between the tabs of a
- * TabHost through fragments, using FragmentTabHost.
- */
-public class FragmentTabs extends FragmentActivity {
- private FragmentTabHost mTabHost;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- setContentView(R.layout.fragment_tabs);
- mTabHost = (FragmentTabHost)findViewById(android.R.id.tabhost);
- mTabHost.setup(this, getSupportFragmentManager(), R.id.realtabcontent);
-
- mTabHost.addTab(mTabHost.newTabSpec("simple").setIndicator("Simple"),
- FragmentStackSupport.CountingFragment.class, null);
- mTabHost.addTab(mTabHost.newTabSpec("contacts").setIndicator("Contacts"),
- LoaderCursorSupport.CursorLoaderListFragment.class, null);
- mTabHost.addTab(mTabHost.newTabSpec("custom").setIndicator("Custom"),
- LoaderCustomSupport.AppListFragment.class, null);
- mTabHost.addTab(mTabHost.newTabSpec("throttle").setIndicator("Throttle"),
- LoaderThrottleSupport.ThrottledLoaderListFragment.class, null);
- }
-}
//END_INCLUDE(complete)
diff --git a/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentTabsFragmentSupport.java b/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentTabsFragmentSupport.java
index 06a132b..61b37b7 100644
--- a/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentTabsFragmentSupport.java
+++ b/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentTabsFragmentSupport.java
@@ -16,42 +16,4 @@
package com.example.android.supportv4.app;
//BEGIN_INCLUDE(complete)
-
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentTabHost;
-
-import com.example.android.supportv4.R;
-
-public class FragmentTabsFragmentSupport extends Fragment {
- private FragmentTabHost mTabHost;
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- mTabHost = new FragmentTabHost(getActivity());
- mTabHost.setup(getActivity(), getChildFragmentManager(), R.id.fragment1);
-
- mTabHost.addTab(mTabHost.newTabSpec("simple").setIndicator("Simple"),
- FragmentStackSupport.CountingFragment.class, null);
- mTabHost.addTab(mTabHost.newTabSpec("contacts").setIndicator("Contacts"),
- LoaderCursorSupport.CursorLoaderListFragment.class, null);
- mTabHost.addTab(mTabHost.newTabSpec("custom").setIndicator("Custom"),
- LoaderCustomSupport.AppListFragment.class, null);
- mTabHost.addTab(mTabHost.newTabSpec("throttle").setIndicator("Throttle"),
- LoaderThrottleSupport.ThrottledLoaderListFragment.class, null);
-
- return mTabHost;
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- mTabHost = null;
- }
-}
//END_INCLUDE(complete)
diff --git a/samples/Support4Demos/src/main/res/layout/fragment_tabs.xml b/samples/Support4Demos/src/main/res/layout/fragment_tabs.xml
deleted file mode 100644
index faea9cc..0000000
--- a/samples/Support4Demos/src/main/res/layout/fragment_tabs.xml
+++ /dev/null
@@ -1,54 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/* //device/apps/common/assets/res/layout/tab_content.xml
-**
-** Copyright 2011, 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.
-*/
--->
-
-<!-- BEGIN_INCLUDE(complete) -->
-<androidx.fragment.app.FragmentTabHost
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@android:id/tabhost"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <LinearLayout
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <TabWidget
- android:id="@android:id/tabs"
- android:orientation="horizontal"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_weight="0"/>
-
- <FrameLayout
- android:id="@android:id/tabcontent"
- android:layout_width="0dp"
- android:layout_height="0dp"
- android:layout_weight="0"/>
-
- <FrameLayout
- android:id="@+id/realtabcontent"
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1"/>
-
- </LinearLayout>
-</androidx.fragment.app.FragmentTabHost>
-<!-- END_INCLUDE(complete) -->
diff --git a/samples/SupportCarDemos/src/main/java/com/example/androidx/car/SwitchListItemActivity.java b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/SwitchListItemActivity.java
index 7b9a95a..501b20e 100644
--- a/samples/SupportCarDemos/src/main/java/com/example/androidx/car/SwitchListItemActivity.java
+++ b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/SwitchListItemActivity.java
@@ -18,6 +18,7 @@
import android.app.Activity;
import android.content.Context;
+import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.widget.CompoundButton;
import android.widget.Toast;
@@ -103,6 +104,24 @@
mItems.add(item);
item = new SwitchListItem(mContext);
+ item.setPrimaryActionIcon(
+ Icon.createWithResource(mContext, android.R.drawable.sym_def_app_icon),
+ SwitchListItem.PRIMARY_ACTION_ICON_SIZE_SMALL);
+ item.setTitle("Switch with Icon");
+ item.setBody(longText);
+ item.setSwitchOnCheckedChangeListener(mListener);
+ mItems.add(item);
+
+ item = new SwitchListItem(mContext);
+ item.setTitle("Switch with Drawable");
+ item.setPrimaryActionIcon(
+ mContext.getDrawable(android.R.drawable.sym_def_app_icon),
+ SwitchListItem.PRIMARY_ACTION_ICON_SIZE_SMALL);
+ item.setBody(longText);
+ item.setSwitchOnCheckedChangeListener(mListener);
+ mItems.add(item);
+
+ item = new SwitchListItem(mContext);
item.setTitle("Clicking item toggles switch");
item.setClickable(true);
item.setSwitchOnCheckedChangeListener(mListener);
diff --git a/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoPlayerActivity.java b/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoPlayerActivity.java
index 11e0280..0d5594dd 100644
--- a/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoPlayerActivity.java
+++ b/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoPlayerActivity.java
@@ -128,7 +128,7 @@
if (intent == null || (videoUri = intent.getData()) == null || !videoUri.isAbsolute()) {
errorString = "Invalid intent";
} else {
- UriMediaItem mediaItem = new UriMediaItem.Builder(this, videoUri).build();
+ UriMediaItem mediaItem = new UriMediaItem.Builder(videoUri).build();
mVideoView.setMediaItem(mediaItem);
mMediaControlView = new MediaControlView(this);
diff --git a/settings.gradle b/settings.gradle
index de45149..4e26b66 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -99,6 +99,7 @@
includeProject(":lifecycle:lifecycle-reactivestreams", "lifecycle/reactivestreams")
includeProject(":lifecycle:lifecycle-reactivestreams-ktx", "lifecycle/reactivestreams/ktx")
includeProject(":lifecycle:lifecycle-runtime", "lifecycle/runtime")
+includeProject(":lifecycle:lifecycle-runtime-eap", "lifecycle/runtime/eap") // temporary
includeProject(":lifecycle:lifecycle-service", "lifecycle/service")
includeProject(":lifecycle:lifecycle-viewmodel", "lifecycle/viewmodel")
includeProject(":lifecycle:lifecycle-viewmodel-ktx", "lifecycle/viewmodel/ktx")
@@ -146,6 +147,7 @@
includeProject(":room:integration-tests:room-testapp-kotlin", "room/integration-tests/kotlintestapp")
includeProject(":room:room-benchmark", "room/benchmark")
includeProject(":room:room-common", "room/common")
+includeProject(":room:room-common-java8", "room/common-java8")
includeProject(":room:room-compiler", "room/compiler")
includeProject(":room:room-guava", "room/guava")
includeProject(":room:room-ktx", "room/ktx")
diff --git a/slices/view/src/main/java/androidx/slice/widget/RowView.java b/slices/view/src/main/java/androidx/slice/widget/RowView.java
index aa64268..76ea8c1 100644
--- a/slices/view/src/main/java/androidx/slice/widget/RowView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/RowView.java
@@ -144,10 +144,15 @@
Handler mHandler;
@SuppressWarnings("WeakerAccess") /* synthetic access */
long mLastSentRangeUpdate;
+ // TODO: mRangeValue is in 0..(mRangeMaxValue-mRangeMinValue) at initialization, and in
+ // mRangeMinValue..mRangeMaxValue after user interaction. As far as I know, this doesn't
+ // cause any incorrect behavior, but it is confusing and error-prone.
@SuppressWarnings("WeakerAccess") /* synthetic access */
int mRangeValue;
@SuppressWarnings("WeakerAccess") /* synthetic access */
int mRangeMinValue;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ int mRangeMaxValue;
private SliceItem mRangeItem;
private int mImageSize;
@@ -427,12 +432,10 @@
if (mRowAction != null) {
setViewClickable(mRootView, true);
}
+ mRangeItem = range;
if (!skipSliderUpdate) {
- determineRangeValues(range);
- addRange(range);
- } else {
- // Even if we're skipping the update, we should still update the range item
- mRangeItem = range;
+ setRangeBounds();
+ addRange();
}
return;
}
@@ -602,14 +605,7 @@
}
}
- private void determineRangeValues(SliceItem rangeItem) {
- if (rangeItem == null) {
- mRangeMinValue = 0;
- mRangeValue = 0;
- return;
- }
- mRangeItem = rangeItem;
-
+ private void setRangeBounds() {
SliceItem min = SliceQuery.findSubtype(mRangeItem, FORMAT_INT, SUBTYPE_MIN);
int minValue = 0;
if (min != null) {
@@ -617,18 +613,27 @@
}
mRangeMinValue = minValue;
- SliceItem progress = SliceQuery.findSubtype(mRangeItem, FORMAT_INT, SUBTYPE_VALUE);
- if (progress != null) {
- mRangeValue = progress.getInt() - minValue;
+ SliceItem max = SliceQuery.findSubtype(mRangeItem, FORMAT_INT, SUBTYPE_MAX);
+ int maxValue = 100; // TODO: This default shouldn't be hardcoded here.
+ if (max != null) {
+ maxValue = max.getInt();
}
+ mRangeMaxValue = maxValue;
+
+ SliceItem progress = SliceQuery.findSubtype(mRangeItem, FORMAT_INT, SUBTYPE_VALUE);
+ int progressValue = 0;
+ if (progress != null) {
+ progressValue = progress.getInt() - minValue;
+ }
+ mRangeValue = progressValue;
}
- private void addRange(final SliceItem range) {
+ private void addRange() {
if (mHandler == null) {
mHandler = new Handler();
}
- final boolean isSeekBar = FORMAT_ACTION.equals(range.getFormat());
+ final boolean isSeekBar = FORMAT_ACTION.equals(mRangeItem.getFormat());
final ProgressBar progressBar = isSeekBar
? new SeekBar(getContext())
: new ProgressBar(getContext(), null, android.R.attr.progressBarStyleHorizontal);
@@ -637,10 +642,9 @@
DrawableCompat.setTint(progressDrawable, mTintColor);
progressBar.setProgressDrawable(progressDrawable);
}
- SliceItem max = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_MAX);
- if (max != null) {
- progressBar.setMax(max.getInt() - mRangeMinValue);
- }
+ // N.B. We don't use progressBar.setMin because it doesn't work properly in backcompat
+ // and/or sliders.
+ progressBar.setMax(mRangeMaxValue - mRangeMinValue);
progressBar.setProgress(mRangeValue);
progressBar.setVisibility(View.VISIBLE);
addView(progressBar);
@@ -664,21 +668,23 @@
}
void sendSliderValue() {
- if (mRangeItem != null) {
- try {
- mLastSentRangeUpdate = System.currentTimeMillis();
- mRangeItem.fireAction(getContext(),
- new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
- .putExtra(EXTRA_RANGE_VALUE, mRangeValue));
- if (mObserver != null) {
- EventInfo info = new EventInfo(getMode(), ACTION_TYPE_SLIDER, ROW_TYPE_SLIDER,
- mRowIndex);
- info.state = mRangeValue;
- mObserver.onSliceAction(info, mRangeItem);
- }
- } catch (CanceledException e) {
- Log.e(TAG, "PendingIntent for slice cannot be sent", e);
+ if (mRangeItem == null) {
+ return;
+ }
+
+ try {
+ mLastSentRangeUpdate = System.currentTimeMillis();
+ mRangeItem.fireAction(getContext(),
+ new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
+ .putExtra(EXTRA_RANGE_VALUE, mRangeValue));
+ if (mObserver != null) {
+ EventInfo info = new EventInfo(getMode(), ACTION_TYPE_SLIDER, ROW_TYPE_SLIDER,
+ mRowIndex);
+ info.state = mRangeValue;
+ mObserver.onSliceAction(info, mRangeItem);
}
+ } catch (CanceledException e) {
+ Log.e(TAG, "PendingIntent for slice cannot be sent", e);
}
}
@@ -904,6 +910,7 @@
mRangeHasPendingUpdate = false;
mRangeItem = null;
mRangeMinValue = 0;
+ mRangeMaxValue = 0;
mRangeValue = 0;
mLastSentRangeUpdate = 0;
mHandler = null;
diff --git a/testutils/src/main/java/androidx/testutils/FragmentActivityUtils.java b/testutils/src/main/java/androidx/testutils/FragmentActivityUtils.java
index 22d0edd..615d1a2 100644
--- a/testutils/src/main/java/androidx/testutils/FragmentActivityUtils.java
+++ b/testutils/src/main/java/androidx/testutils/FragmentActivityUtils.java
@@ -90,8 +90,8 @@
ActivityTestRule<? extends RecreatedActivity> rule, final T activity)
throws InterruptedException {
// Now switch the orientation
- RecreatedActivity.sResumed = new CountDownLatch(1);
- RecreatedActivity.sDestroyed = new CountDownLatch(1);
+ RecreatedActivity.setResumedLatch(new CountDownLatch(1));
+ RecreatedActivity.setDestroyedLatch(new CountDownLatch(1));
runOnUiThreadRethrow(rule, new Runnable() {
@Override
@@ -99,9 +99,9 @@
activity.recreate();
}
});
- assertTrue(RecreatedActivity.sResumed.await(1, TimeUnit.SECONDS));
- assertTrue(RecreatedActivity.sDestroyed.await(1, TimeUnit.SECONDS));
- T newActivity = (T) RecreatedActivity.sActivity;
+ assertTrue(RecreatedActivity.getResumedLatch().await(1, TimeUnit.SECONDS));
+ assertTrue(RecreatedActivity.getDestroyedLatch().await(1, TimeUnit.SECONDS));
+ T newActivity = (T) RecreatedActivity.getActivity();
waitForExecution(rule);
diff --git a/testutils/src/main/java/androidx/testutils/RecreatedActivity.java b/testutils/src/main/java/androidx/testutils/RecreatedActivity.java
deleted file mode 100644
index 34326a48..0000000
--- a/testutils/src/main/java/androidx/testutils/RecreatedActivity.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2017 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 androidx.testutils;
-
-import android.os.Bundle;
-
-import androidx.annotation.Nullable;
-import androidx.fragment.app.FragmentActivity;
-import androidx.test.rule.ActivityTestRule;
-
-import java.util.concurrent.CountDownLatch;
-
-/**
- * Extension of {@link FragmentActivity} that keeps track of when it is recreated.
- * In order to use this class, have your activity extend it and call
- * {@link FragmentActivityUtils#recreateActivity(ActivityTestRule, RecreatedActivity)} API.
- */
-public class RecreatedActivity extends FragmentActivity {
- // These must be cleared after each test using clearState()
- public static RecreatedActivity sActivity;
- public static CountDownLatch sResumed;
- public static CountDownLatch sDestroyed;
-
- static void clearState() {
- sActivity = null;
- sResumed = null;
- sDestroyed = null;
- }
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- sActivity = this;
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- if (sResumed != null) {
- sResumed.countDown();
- }
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- if (sDestroyed != null) {
- sDestroyed.countDown();
- }
- }
-}
diff --git a/testutils/src/main/java/androidx/testutils/RecreatedActivity.kt b/testutils/src/main/java/androidx/testutils/RecreatedActivity.kt
new file mode 100644
index 0000000..22b74aa
--- /dev/null
+++ b/testutils/src/main/java/androidx/testutils/RecreatedActivity.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2017 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 androidx.testutils
+
+import android.os.Bundle
+import androidx.fragment.app.FragmentActivity
+import java.util.concurrent.CountDownLatch
+
+/**
+ * Extension of [FragmentActivity] that keeps track of when it is recreated.
+ * In order to use this class, have your activity extend it and call
+ * [FragmentActivityUtils.recreateActivity] API.
+ */
+open class RecreatedActivity : FragmentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ activity = this
+ }
+
+ override fun onResume() {
+ super.onResume()
+ resumedLatch?.countDown()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ destroyedLatch?.countDown()
+ }
+
+ companion object {
+ // These must be cleared after each test using clearState()
+ @JvmStatic
+ var activity: RecreatedActivity? = null
+ @JvmStatic
+ var resumedLatch: CountDownLatch? = null
+ @JvmStatic
+ var destroyedLatch: CountDownLatch? = null
+
+ @JvmStatic
+ fun clearState() {
+ activity = null
+ resumedLatch = null
+ destroyedLatch = null
+ }
+ }
+}
diff --git a/textclassifier/api/1.0.0-alpha03.txt b/textclassifier/api/1.0.0-alpha03.txt
index 043c858..c37c7d2 100644
--- a/textclassifier/api/1.0.0-alpha03.txt
+++ b/textclassifier/api/1.0.0-alpha03.txt
@@ -1,6 +1,10 @@
// Signature format: 3.0
package androidx.textclassifier {
+ public final class ExtrasUtils {
+ method public static java.util.Locale? getTopLanguage(android.content.Intent?);
+ }
+
public final class TextClassification {
method public static androidx.textclassifier.TextClassification createFromBundle(android.os.Bundle);
method public java.util.List<androidx.core.app.RemoteActionCompat> getActions();
diff --git a/textclassifier/api/current.txt b/textclassifier/api/current.txt
index 043c858..c37c7d2 100644
--- a/textclassifier/api/current.txt
+++ b/textclassifier/api/current.txt
@@ -1,6 +1,10 @@
// Signature format: 3.0
package androidx.textclassifier {
+ public final class ExtrasUtils {
+ method public static java.util.Locale? getTopLanguage(android.content.Intent?);
+ }
+
public final class TextClassification {
method public static androidx.textclassifier.TextClassification createFromBundle(android.os.Bundle);
method public java.util.List<androidx.core.app.RemoteActionCompat> getActions();
diff --git a/textclassifier/src/androidTest/java/androidx/textclassifier/ExtrasUtilsTest.java b/textclassifier/src/androidTest/java/androidx/textclassifier/ExtrasUtilsTest.java
new file mode 100644
index 0000000..17511e0
--- /dev/null
+++ b/textclassifier/src/androidTest/java/androidx/textclassifier/ExtrasUtilsTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2019 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 androidx.textclassifier;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Intent;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for {@link ExtrasUtils}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ExtrasUtilsTest {
+
+ @Test
+ public void testGetTopLanguage() {
+ final Intent intent = ExtrasUtils.buildFakeTextClassifierIntent("ja", "en");
+ assertThat(ExtrasUtils.getTopLanguage(intent).getLanguage()).isEqualTo("ja");
+ }
+
+ @Test
+ public void testGetTopLanguage_differentLanguage() {
+ final Intent intent = ExtrasUtils.buildFakeTextClassifierIntent("de");
+ assertThat(ExtrasUtils.getTopLanguage(intent).getLanguage()).isEqualTo("de");
+ }
+
+ @Test
+ public void testGetTopLanguage_nullLanguageBundle() {
+ assertThat(ExtrasUtils.getTopLanguage(new Intent())).isNull();
+ }
+
+ @Test
+ public void testGetTopLanguage_null() {
+ assertThat(ExtrasUtils.getTopLanguage(null)).isNull();
+ }
+}
diff --git a/textclassifier/src/main/java/androidx/textclassifier/BundleUtils.java b/textclassifier/src/main/java/androidx/textclassifier/BundleUtils.java
index 1906b4c..52704ec 100644
--- a/textclassifier/src/main/java/androidx/textclassifier/BundleUtils.java
+++ b/textclassifier/src/main/java/androidx/textclassifier/BundleUtils.java
@@ -25,6 +25,7 @@
import androidx.collection.ArrayMap;
import androidx.core.app.RemoteActionCompat;
import androidx.core.os.LocaleListCompat;
+import androidx.versionedparcelable.ParcelUtils;
import java.util.ArrayList;
import java.util.List;
@@ -80,30 +81,13 @@
/** Serializes a list of actions to a bundle, or clears it if null is passed. */
static void putRemoteActionList(
@NonNull Bundle bundle, @NonNull String key,
- @Nullable List<RemoteActionCompat> actions) {
- if (actions == null) {
- bundle.remove(key);
- return;
- }
- final ArrayList<Bundle> actionBundles = new ArrayList<>(actions.size());
- for (RemoteActionCompat action : actions) {
- actionBundles.add(action.toBundle());
- }
- bundle.putParcelableArrayList(key, actionBundles);
+ @NonNull List<RemoteActionCompat> actions) {
+ ParcelUtils.putVersionedParcelableList(bundle, key, actions);
}
- /** @throws IllegalArgumentException if key can't be found in the bundle */
static List<RemoteActionCompat> getRemoteActionListOrThrow(
@NonNull Bundle bundle, @NonNull String key) {
- final List<Bundle> actionBundles = bundle.getParcelableArrayList(key);
- if (actionBundles == null) {
- throw new IllegalArgumentException("Missing " + key);
- }
- final List<RemoteActionCompat> actions = new ArrayList<>(actionBundles.size());
- for (Bundle actionBundle : actionBundles) {
- actions.add(RemoteActionCompat.createFromBundle(actionBundle));
- }
- return actions;
+ return ParcelUtils.getVersionedParcelableList(bundle, key);
}
/** Serializes a list of TextLinks to a bundle, or clears it if null is passed. */
diff --git a/textclassifier/src/main/java/androidx/textclassifier/ExtrasUtils.java b/textclassifier/src/main/java/androidx/textclassifier/ExtrasUtils.java
new file mode 100644
index 0000000..26c45c6
--- /dev/null
+++ b/textclassifier/src/main/java/androidx/textclassifier/ExtrasUtils.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2019 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 androidx.textclassifier;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.core.os.LocaleListCompat;
+
+import java.util.Locale;
+
+/**
+ * Utilities for inserting/retrieving data into/from textclassifier related results and intents.
+ */
+public final class ExtrasUtils {
+
+ private static final String TAG = "ExtrasUtils";
+
+ private static final String EXTRA_FROM_TEXT_CLASSIFIER =
+ "android.view.textclassifier.extra.FROM_TEXT_CLASSIFIER";
+ private static final String ENTITY_TYPE = "entity-type";
+ private static final String SCORE = "score";
+ private static final String TEXT_LANGUAGES = "text-languages";
+
+ private ExtrasUtils() {}
+
+ /**
+ * Returns the highest scoring language found in the textclassifier extras in the intent.
+ * This may return null if the data could not be found.
+ *
+ * @param intent the intent used to start the activity.
+ * @see android.app.Activity#getIntent()
+ */
+ @Nullable
+ public static Locale getTopLanguage(@Nullable Intent intent) {
+ try {
+ // NOTE: This is (and should be) a copy of the related platform code.
+ // It is hard to test this code returns something on a given platform because we can't
+ // guarantee the TextClassifier implementation that will be used to send the intent.
+ // Depend on the platform tests instead and avoid this code running out of sync with
+ // what is expected of each platform. Note that the code may differ from platform to
+ // platform but that will be a bad idea as it will be hard to manage.
+ // TODO: Include a "put" counterpart of this method so that other TextClassifier
+ // implementations may use it to put language data into the generated intent in a way
+ // that this method can retrieve it.
+ if (intent == null) {
+ return null;
+ }
+ final Bundle tcBundle = intent.getBundleExtra(EXTRA_FROM_TEXT_CLASSIFIER);
+ if (tcBundle == null) {
+ return null;
+ }
+ final Bundle textLanguagesExtra = tcBundle.getBundle(TEXT_LANGUAGES);
+ if (textLanguagesExtra == null) {
+ return null;
+ }
+ final String[] languages = textLanguagesExtra.getStringArray(ENTITY_TYPE);
+ final float[] scores = textLanguagesExtra.getFloatArray(SCORE);
+ if (languages == null || scores == null
+ || languages.length == 0 || languages.length != scores.length) {
+ return null;
+ }
+ int highestScoringIndex = 0;
+ for (int i = 1; i < languages.length; i++) {
+ if (scores[highestScoringIndex] < scores[i]) {
+ highestScoringIndex = i;
+ }
+ }
+ final LocaleListCompat localeList =
+ LocaleListCompat.forLanguageTags(languages[highestScoringIndex]);
+ return localeList.isEmpty() ? null : localeList.get(0);
+ } catch (Throwable t) {
+ // Prevent this method from crashing the process.
+ Log.e(TAG, "Error retrieving language information from textclassifier intent", t);
+ return null;
+ }
+ }
+
+ /**
+ * Returns a fake TextClassifier generated intent for testing purposes.
+ * @param languages ordered list of languages for the classified text
+ */
+ @VisibleForTesting
+ static Intent buildFakeTextClassifierIntent(String... languages) {
+ final float[] scores = new float[languages.length];
+ float scoresLeft = 1f;
+ for (int i = 0; i < scores.length; i++) {
+ scores[i] = scoresLeft /= 2;
+ }
+ final Bundle textLanguagesExtra = new Bundle();
+ textLanguagesExtra.putStringArray(ENTITY_TYPE, languages);
+ textLanguagesExtra.putFloatArray(SCORE, scores);
+ final Bundle tcBundle = new Bundle();
+ tcBundle.putBundle(TEXT_LANGUAGES, textLanguagesExtra);
+ return new Intent(Intent.ACTION_VIEW).putExtra(EXTRA_FROM_TEXT_CLASSIFIER, tcBundle);
+ }
+}
diff --git a/versionedparcelable/api/1.1.0-alpha02.txt b/versionedparcelable/api/1.1.0-alpha02.txt
index a53654a..0ca14c1 100644
--- a/versionedparcelable/api/1.1.0-alpha02.txt
+++ b/versionedparcelable/api/1.1.0-alpha02.txt
@@ -3,7 +3,9 @@
public class ParcelUtils {
method public static <T extends androidx.versionedparcelable.VersionedParcelable> T? getVersionedParcelable(android.os.Bundle!, String!);
+ method public static <T extends androidx.versionedparcelable.VersionedParcelable> java.util.List<T>? getVersionedParcelableList(android.os.Bundle!, String!);
method public static void putVersionedParcelable(android.os.Bundle, String, androidx.versionedparcelable.VersionedParcelable);
+ method public static void putVersionedParcelableList(android.os.Bundle, String, java.util.List<? extends androidx.versionedparcelable.VersionedParcelable>);
}
public interface VersionedParcelable {
diff --git a/versionedparcelable/api/current.txt b/versionedparcelable/api/current.txt
index a53654a..0ca14c1 100644
--- a/versionedparcelable/api/current.txt
+++ b/versionedparcelable/api/current.txt
@@ -3,7 +3,9 @@
public class ParcelUtils {
method public static <T extends androidx.versionedparcelable.VersionedParcelable> T? getVersionedParcelable(android.os.Bundle!, String!);
+ method public static <T extends androidx.versionedparcelable.VersionedParcelable> java.util.List<T>? getVersionedParcelableList(android.os.Bundle!, String!);
method public static void putVersionedParcelable(android.os.Bundle, String, androidx.versionedparcelable.VersionedParcelable);
+ method public static void putVersionedParcelableList(android.os.Bundle, String, java.util.List<? extends androidx.versionedparcelable.VersionedParcelable>);
}
public interface VersionedParcelable {
diff --git a/versionedparcelable/src/main/java/androidx/versionedparcelable/ParcelUtils.java b/versionedparcelable/src/main/java/androidx/versionedparcelable/ParcelUtils.java
index b9b3b04..c2d530b 100644
--- a/versionedparcelable/src/main/java/androidx/versionedparcelable/ParcelUtils.java
+++ b/versionedparcelable/src/main/java/androidx/versionedparcelable/ParcelUtils.java
@@ -27,6 +27,8 @@
import java.io.InputStream;
import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
/**
* Utilities for managing {@link VersionedParcelable}s.
@@ -91,7 +93,6 @@
b.putParcelable(key, innerBundle);
}
-
/**
* Get a VersionedParcelable from a Bundle.
*
@@ -109,4 +110,43 @@
return null;
}
}
+
+ /**
+ * Add a list of VersionedParcelable to an existing Bundle.
+ */
+ public static void putVersionedParcelableList(@NonNull Bundle b, @NonNull String key,
+ @NonNull List<? extends VersionedParcelable> list) {
+ Bundle innerBundle = new Bundle();
+ ArrayList<Parcelable> toWrite = new ArrayList<>();
+ for (VersionedParcelable obj : list) {
+ toWrite.add(toParcelable(obj));
+ }
+ innerBundle.putParcelableArrayList(INNER_BUNDLE_KEY, toWrite);
+ b.putParcelable(key, innerBundle);
+ }
+
+ /**
+ * Get a list of VersionedParcelable from a Bundle.
+ *
+ * Returns null if the bundle isn't present or ClassLoader issues occur.
+ */
+ @SuppressWarnings("TypeParameterUnusedInFormals")
+ @Nullable
+ public static <T extends VersionedParcelable> List<T> getVersionedParcelableList(
+ Bundle bundle, String key) {
+ List<T> resultList = new ArrayList<>();
+ try {
+ Bundle innerBundle = bundle.getParcelable(key);
+ innerBundle.setClassLoader(ParcelUtils.class.getClassLoader());
+ ArrayList<Parcelable> parcelableArrayList =
+ innerBundle.getParcelableArrayList(INNER_BUNDLE_KEY);
+ for (Parcelable parcelable : parcelableArrayList) {
+ resultList.add((T) fromParcelable(parcelable));
+ }
+ return resultList;
+ } catch (RuntimeException e) {
+ // There may be new classes or such in the bundle, make sure not to crash the caller.
+ }
+ return null;
+ }
}
diff --git a/viewpager2/integration-tests/testapp/build.gradle b/viewpager2/integration-tests/testapp/build.gradle
index be2719a..3affaad 100644
--- a/viewpager2/integration-tests/testapp/build.gradle
+++ b/viewpager2/integration-tests/testapp/build.gradle
@@ -33,6 +33,7 @@
implementation(project(":viewpager2"))
implementation(ARCH_LIFECYCLE_EXTENSIONS)
implementation(MATERIAL)
+ implementation(project(":coordinatorlayout"))
implementation(project(":cardview"))
androidTestImplementation(TEST_RULES)
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/BaseTest.kt b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/BaseTest.kt
index 192bc62..a2d04e0 100644
--- a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/BaseTest.kt
+++ b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/BaseTest.kt
@@ -39,7 +39,7 @@
import androidx.test.espresso.matcher.ViewMatchers.withParent
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.rule.ActivityTestRule
-import androidx.testutils.FragmentActivityUtils
+import androidx.testutils.AppCompatActivityUtils
import androidx.testutils.FragmentActivityUtils.waitForActivityDrawn
import androidx.viewpager2.LocaleTestUtils
import androidx.viewpager2.adapter.FragmentStateAdapter
@@ -117,7 +117,7 @@
viewPager.adapter = adapterProvider(activity)
onCreateCallback(viewPager)
}
- activity = FragmentActivityUtils.recreateActivity(activityTestRule, activity)
+ activity = AppCompatActivityUtils.recreateActivity(activityTestRule, activity)
TestActivity.onCreateCallback = { }
waitForActivityDrawn(activity)
}
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/TestActivity.kt b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/TestActivity.kt
index f4ed3c6..7715091 100644
--- a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/TestActivity.kt
+++ b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/TestActivity.kt
@@ -17,11 +17,11 @@
package androidx.viewpager2.widget.swipe
import android.os.Bundle
-import androidx.testutils.RecreatedActivity
+import androidx.testutils.RecreatedAppCompatActivity
import androidx.viewpager2.LocaleTestUtils
import androidx.viewpager2.test.R
-class TestActivity : RecreatedActivity() {
+class TestActivity : RecreatedAppCompatActivity() {
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (intent?.hasExtra(EXTRA_LANGUAGE) == true) {
diff --git a/viewpager2/src/androidTest/res/values/styles.xml b/viewpager2/src/androidTest/res/values/styles.xml
index 94e0a86..cb1f73d 100644
--- a/viewpager2/src/androidTest/res/values/styles.xml
+++ b/viewpager2/src/androidTest/res/values/styles.xml
@@ -15,7 +15,7 @@
-->
<resources>
- <style name="TestActivityTheme" parent="android:Theme">
+ <style name="TestActivityTheme" parent="Theme.AppCompat">
<item name="android:windowAnimationStyle">@empty</item>
</style>
</resources>
diff --git a/webkit/src/androidTest/java/androidx/webkit/WebViewAssetLoaderIntegrationTest.java b/webkit/src/androidTest/java/androidx/webkit/WebViewAssetLoaderIntegrationTest.java
index e1fe81f..2ec6943 100644
--- a/webkit/src/androidTest/java/androidx/webkit/WebViewAssetLoaderIntegrationTest.java
+++ b/webkit/src/androidTest/java/androidx/webkit/WebViewAssetLoaderIntegrationTest.java
@@ -17,25 +17,22 @@
package androidx.webkit;
import android.app.Activity;
-import android.net.Uri;
import android.os.Bundle;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
-import android.webkit.WebViewClient;
import androidx.test.filters.MediumTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
+import org.junit.After;
import org.junit.Assert;
+import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
-import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.Callable;
-
@RunWith(AndroidJUnit4.class)
public class WebViewAssetLoaderIntegrationTest {
private static final String TAG = "WebViewAssetLoaderIntegrationTest";
@@ -44,67 +41,58 @@
public final ActivityTestRule<TestActivity> mActivityRule =
new ActivityTestRule<>(TestActivity.class);
+ private WebViewOnUiThread mOnUiThread;
+ private WebViewAssetLoader mAssetLoader;
+
+ private static class AssetLoadingWebViewClient extends WebViewOnUiThread.WaitForLoadedClient {
+ private final WebViewAssetLoader mAssetLoader;
+ AssetLoadingWebViewClient(WebViewOnUiThread onUiThread,
+ WebViewAssetLoader assetLoader) {
+ super(onUiThread);
+ mAssetLoader = assetLoader;
+ }
+
+ @SuppressWarnings({"deprecated"})
+ @Override
+ public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
+ return mAssetLoader.shouldInterceptRequest(url);
+ }
+
+ @Override
+ public WebResourceResponse shouldInterceptRequest(WebView view,
+ WebResourceRequest request) {
+ return mAssetLoader.shouldInterceptRequest(request);
+ }
+ }
+
// An Activity for Integeration tests
public static class TestActivity extends Activity {
- private class MyWebViewClient extends WebViewClient {
- @Override
- public boolean shouldOverrideUrlLoading(WebView view, String url) {
- return false;
- }
-
- @Override
- public void onPageFinished(WebView view, String url) {
- mOnPageFinishedUrl.add(url);
- }
-
- @SuppressWarnings({"deprecated"})
- @Override
- public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
- return mAssetLoader.shouldInterceptRequest(url);
- }
-
- @Override
- public WebResourceResponse shouldInterceptRequest(WebView view,
- WebResourceRequest request) {
- return mAssetLoader.shouldInterceptRequest(request);
- }
- }
-
- private WebViewAssetLoader mAssetLoader;
private WebView mWebView;
- private ArrayBlockingQueue<String> mOnPageFinishedUrl = new ArrayBlockingQueue<String>(5);
-
- public WebViewAssetLoader getAssetLoader() {
- return mAssetLoader;
-
- }
public WebView getWebView() {
return mWebView;
}
- public ArrayBlockingQueue<String> getOnPageFinishedUrl() {
- return mOnPageFinishedUrl;
- }
-
- private void setUpWebView(WebView view) {
- view.setWebViewClient(new MyWebViewClient());
- }
-
+ // Runs before test suite's @Before.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- mAssetLoader = (new WebViewAssetLoader.Builder(this)).build();
mWebView = new WebView(this);
- setUpWebView(mWebView);
setContentView(mWebView);
}
+ }
- @Override
- protected void onDestroy() {
- super.onDestroy();
- mWebView.destroy();
- mWebView = null;
+ @Before
+ public void setUp() {
+ mAssetLoader = (new WebViewAssetLoader.Builder(mActivityRule.getActivity())).build();
+ mOnUiThread = new WebViewOnUiThread(mActivityRule.getActivity().getWebView());
+ mOnUiThread.setWebViewClient(new AssetLoadingWebViewClient(mOnUiThread, mAssetLoader));
+ }
+
+ @After
+ public void tearDown() {
+ if (mOnUiThread != null) {
+ mOnUiThread.cleanUp();
}
}
@@ -112,66 +100,34 @@
@MediumTest
public void testAssetHosting() throws Exception {
final TestActivity activity = mActivityRule.getActivity();
- final String test_with_title_path = "www/test_with_title.html";
+ final String testWithTitlePath = "www/test_with_title.html";
- String url = WebkitUtils.onMainThreadSync(new Callable<String>() {
- @Override
- public String call() {
- WebViewAssetLoader assetLoader = activity.getAssetLoader();
- Uri.Builder testPath =
- assetLoader.getAssetsHttpsPrefix().buildUpon()
- .appendPath(test_with_title_path);
+ String url =
+ mAssetLoader.getAssetsHttpsPrefix().buildUpon()
+ .appendPath(testWithTitlePath)
+ .build()
+ .toString();
- String url = testPath.toString();
- activity.getWebView().loadUrl(url);
+ mOnUiThread.loadUrlAndWaitForCompletion(url);
- return url;
- }
- });
-
- String onPageFinishedUrl = activity.getOnPageFinishedUrl().take();
- Assert.assertEquals(url, onPageFinishedUrl);
-
- String title = WebkitUtils.onMainThreadSync(new Callable<String>() {
- @Override
- public String call() {
- return activity.getWebView().getTitle();
- }
- });
- Assert.assertEquals("WebViewAssetLoaderTest", title);
+ Assert.assertEquals("WebViewAssetLoaderTest", mOnUiThread.getTitle());
}
@Test
@MediumTest
public void testResourcesHosting() throws Exception {
final TestActivity activity = mActivityRule.getActivity();
- final String test_with_title_path = "test_with_title.html";
+ final String testWithTitlePath = "test_with_title.html";
- String url = WebkitUtils.onMainThreadSync(new Callable<String>() {
- @Override
- public String call() {
- WebViewAssetLoader assetLoader = activity.getAssetLoader();
- Uri.Builder testPath =
- assetLoader.getResourcesHttpsPrefix().buildUpon()
- .appendPath("raw")
- .appendPath(test_with_title_path);
+ String url =
+ mAssetLoader.getResourcesHttpsPrefix().buildUpon()
+ .appendPath("raw")
+ .appendPath(testWithTitlePath)
+ .build()
+ .toString();
- String url = testPath.toString();
- activity.getWebView().loadUrl(url);
+ mOnUiThread.loadUrlAndWaitForCompletion(url);
- return url;
- }
- });
-
- String onPageFinishedUrl = activity.getOnPageFinishedUrl().take();
- Assert.assertEquals(url, onPageFinishedUrl);
-
- String title = WebkitUtils.onMainThreadSync(new Callable<String>() {
- @Override
- public String call() {
- return activity.getWebView().getTitle();
- }
- });
- Assert.assertEquals("WebViewAssetLoaderTest", title);
+ Assert.assertEquals("WebViewAssetLoaderTest", mOnUiThread.getTitle());
}
}
diff --git a/webkit/src/androidTest/java/androidx/webkit/WebViewAssetLoaderTest.java b/webkit/src/androidTest/java/androidx/webkit/WebViewAssetLoaderTest.java
index ff62e4b..bf663db 100644
--- a/webkit/src/androidTest/java/androidx/webkit/WebViewAssetLoaderTest.java
+++ b/webkit/src/androidTest/java/androidx/webkit/WebViewAssetLoaderTest.java
@@ -18,7 +18,6 @@
import android.content.ContextWrapper;
import android.net.Uri;
-import android.util.Log;
import android.webkit.WebResourceResponse;
import androidx.test.core.app.ApplicationProvider;
@@ -95,9 +94,8 @@
try {
return new ByteArrayInputStream(contents.getBytes(encoding));
} catch (UnsupportedEncodingException e) {
- Log.e(TAG, "exception when creating response", e);
+ throw new RuntimeException(e);
}
- return null;
}
};
@@ -106,10 +104,11 @@
WebResourceResponse response =
assetLoader.shouldInterceptRequest("http://appassets.androidplatform.net/test/");
- Assert.assertNotNull(response);
+ Assert.assertNotNull("didn't match the exact registered URL", response);
Assert.assertEquals(contents, readAsString(response.getData(), encoding));
- Assert.assertNull(assetLoader.shouldInterceptRequest("http://foo.bar/"));
+ Assert.assertNull("opened a non-registered URL - should return null",
+ assetLoader.shouldInterceptRequest("http://foo.bar/"));
}
@Test
@@ -125,21 +124,23 @@
try {
return new ByteArrayInputStream(testHtmlContents.getBytes("utf-8"));
} catch (IOException e) {
- Log.e(TAG, "Unable to open asset URL: " + url);
- return null;
+ throw new RuntimeException(e);
}
}
return null;
}
});
- Assert.assertNull(assetLoader.getAssetsHttpPrefix());
+ Assert.assertNull("HTTP is not allowed - getAssetsHttpPrefix should return null",
+ assetLoader.getAssetsHttpPrefix());
Assert.assertEquals(assetLoader.getAssetsHttpsPrefix(),
Uri.parse("https://appassets.androidplatform.net/assets/"));
WebResourceResponse response =
assetLoader.shouldInterceptRequest("https://appassets.androidplatform.net/assets/www/test.html");
- Assert.assertNotNull(response);
+ Assert.assertNotNull("failed to match the URL and returned null response", response);
+ Assert.assertNotNull("matched the URL but not the file and returned a null InputStream",
+ response.getData());
Assert.assertEquals(testHtmlContents, readAsString(response.getData(), "utf-8"));
}
@@ -152,23 +153,27 @@
WebViewAssetLoader assetLoader = builder.buildForTest(new MockAssetHelper() {
@Override
public InputStream openResource(Uri uri) {
- try {
- if (uri.getPath().equals("raw/test.html")) {
+ if (uri.getPath().equals("raw/test.html")) {
+ try {
return new ByteArrayInputStream(testHtmlContents.getBytes("utf-8"));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
}
- } catch (IOException e) {
- Log.e(TAG, "exception when creating response", e);
}
return null;
}
});
- Assert.assertNull(assetLoader.getResourcesHttpPrefix());
- Assert.assertEquals(assetLoader.getResourcesHttpsPrefix(), Uri.parse("https://appassets.androidplatform.net/res/"));
+ Assert.assertNull("HTTP is not allowed - getResourcesHttpPrefix should return null",
+ assetLoader.getResourcesHttpPrefix());
+ Assert.assertEquals(assetLoader.getResourcesHttpsPrefix(),
+ Uri.parse("https://appassets.androidplatform.net/res/"));
WebResourceResponse response =
assetLoader.shouldInterceptRequest("https://appassets.androidplatform.net/res/raw/test.html");
- Assert.assertNotNull(response);
+ Assert.assertNotNull("failed to match the URL and returned null response", response);
+ Assert.assertNotNull("matched the prefix URL but not the file",
+ response.getData());
Assert.assertEquals(testHtmlContents, readAsString(response.getData(), "utf-8"));
}
@@ -188,8 +193,7 @@
try {
return new ByteArrayInputStream(testHtmlContents.getBytes("utf-8"));
} catch (IOException e) {
- Log.e(TAG, "Unable to open asset URL: " + url);
- return null;
+ throw new RuntimeException(e);
}
}
return null;
@@ -203,7 +207,9 @@
WebResourceResponse response =
assetLoader.shouldInterceptRequest("http://example.com/android_assets/www/test.html");
- Assert.assertNotNull(response);
+ Assert.assertNotNull("failed to match the URL and returned null response", response);
+ Assert.assertNotNull("matched the prefix URL but not the file",
+ response.getData());
Assert.assertEquals(testHtmlContents, readAsString(response.getData(), "utf-8"));
}
@@ -219,12 +225,12 @@
WebViewAssetLoader assetLoader = builder.buildForTest(new MockAssetHelper() {
@Override
public InputStream openResource(Uri uri) {
- try {
- if (uri.getPath().equals("raw/test.html")) {
+ if (uri.getPath().equals("raw/test.html")) {
+ try {
return new ByteArrayInputStream(testHtmlContents.getBytes("utf-8"));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
}
- } catch (IOException e) {
- Log.e(TAG, "exception when creating response", e);
}
return null;
}
@@ -237,7 +243,9 @@
WebResourceResponse response =
assetLoader.shouldInterceptRequest("http://example.com/android_res/raw/test.html");
- Assert.assertNotNull(response);
+ Assert.assertNotNull("failed to match the URL and returned null response", response);
+ Assert.assertNotNull("matched the prefix URL but not the file",
+ response.getData());
Assert.assertEquals(testHtmlContents, readAsString(response.getData(), "utf-8"));
}
}
diff --git a/webkit/src/androidTest/java/androidx/webkit/WebViewOnUiThread.java b/webkit/src/androidTest/java/androidx/webkit/WebViewOnUiThread.java
index 84f52f3..21e687c 100644
--- a/webkit/src/androidTest/java/androidx/webkit/WebViewOnUiThread.java
+++ b/webkit/src/androidTest/java/androidx/webkit/WebViewOnUiThread.java
@@ -30,6 +30,7 @@
import android.webkit.WebView;
import android.webkit.WebViewClient;
+import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
@@ -45,7 +46,7 @@
* Modifications to this class should be reflected in that class as necessary. See
* http://go/modifying-webview-cts.
*/
-class WebViewOnUiThread {
+public class WebViewOnUiThread {
/**
* The maximum time, in milliseconds (10 seconds) to wait for a load
* to be triggered.
@@ -69,10 +70,22 @@
private WebView mWebView;
public WebViewOnUiThread() {
+ this(WebkitUtils.onMainThreadSync(new Callable<WebView>() {
+ @Override
+ public WebView call() {
+ return new WebView(ApplicationProvider.getApplicationContext());
+ }
+ }));
+ }
+
+ /**
+ * Create a new WebViewOnUiThread wrapping the provided {@link WebView}.
+ */
+ public WebViewOnUiThread(final WebView webView) {
WebkitUtils.onMainThreadSync(new Runnable() {
@Override
public void run() {
- mWebView = new WebView(ApplicationProvider.getApplicationContext());
+ mWebView = webView;
mWebView.setWebViewClient(new WaitForLoadedClient(WebViewOnUiThread.this));
mWebView.setWebChromeClient(new WaitForProgressClient(WebViewOnUiThread.this));
}
@@ -520,6 +533,7 @@
}
@Override
+ @CallSuper
public void onProgressChanged(WebView view, int newProgress) {
super.onProgressChanged(view, newProgress);
mOnUiThread.onProgressChanged(newProgress);
@@ -541,12 +555,14 @@
}
@Override
+ @CallSuper
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
mOnUiThread.onPageFinished();
}
@Override
+ @CallSuper
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
mOnUiThread.onPageStarted();
diff --git a/webkit/src/androidTest/java/androidx/webkit/WebViewRenderProcessTest.java b/webkit/src/androidTest/java/androidx/webkit/WebViewRenderProcessTest.java
index c2fc7c9..fda6677 100644
--- a/webkit/src/androidTest/java/androidx/webkit/WebViewRenderProcessTest.java
+++ b/webkit/src/androidTest/java/androidx/webkit/WebViewRenderProcessTest.java
@@ -31,6 +31,7 @@
import com.google.common.util.concurrent.ListenableFuture;
import org.junit.Assert;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -102,11 +103,24 @@
return future;
}
+ @Before
+ public void setUp() {
+ WebkitUtils.checkFeature(WebViewFeature.GET_WEB_VIEW_RENDERER);
+
+ // Ensure that any existing renderer still alive after a previous test is terminated.
+ // TODO(tobiasjs): This assumes that WebView uses at most one renderer, which is true
+ // for now but may not remain so in future.
+ final WebView webView = WebViewOnUiThread.createWebView();
+ final WebViewRenderProcess renderProcess = getRenderProcessOnUiThread(webView);
+ WebViewOnUiThread.destroy(webView);
+ if (renderProcess != null) {
+ terminateRenderProcessOnUiThread(renderProcess);
+ }
+ }
+
@Test
@SdkSuppress(maxSdkVersion = Build.VERSION_CODES.N_MR1)
public void testGetWebViewRenderProcessPreO() throws Throwable {
- WebkitUtils.checkFeature(WebViewFeature.GET_WEB_VIEW_RENDERER);
-
// It should not be possible to get a renderer pre-O
WebView webView = WebViewOnUiThread.createWebView();
final WebViewRenderProcess renderer = startAndGetRenderProcess(webView).get();
@@ -120,7 +134,6 @@
@SuppressLint("NewApi")
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
public void testGetWebViewRenderProcess() throws Throwable {
- WebkitUtils.checkFeature(WebViewFeature.GET_WEB_VIEW_RENDERER);
// TODO(tobiasjs) some O devices are not multiprocess, and multiprocess can also be disabled
// manually. This test should handle those scenarios.