From adfd62c484f6d888cc9339e79619ccf72c3b133f Mon Sep 17 00:00:00 2001 From: Adam Powell Date: Tue, 31 Jan 2017 14:54:07 -0800 Subject: [PATCH] Fragment grab bag! * Fragment#isStateSaved: report whether a given fragment has been through state save within its given FragmentManager. This will additionally echo the state of its child FragmentManager, indicating whether it is safe to perform transactions without losing state. * Fragment#setArguments can now be called at any time before a fragment's state is saved, not just before an internal index is assigned. This fixes an issue with assigning arguments during fragment inflation in some code paths. * FragmentTransaction#postOnCommit: Add a Runnable to a FragmentTransaction that will be executed once the operations in the transaction have been fully executed. Transaction optimization may mean that other transaction ops could run before this is executed, or that specific operations from this transaction were optimized out. (e.g. a fragment in this transaction was added, but then removed in a subsequent transaction before pending operations were executed.) This may not be used for transactions added to the back stack, as we cannot persist the runnable for later to guarantee running it when the transaction is reversed. Test: CTS Change-Id: I4eb90588135e30f287c6487aa62ebda231f3dad3 --- api/current.txt | 2 ++ api/system-current.txt | 2 ++ api/test-current.txt | 2 ++ core/java/android/app/BackStackRecord.java | 24 ++++++++++++++ core/java/android/app/Fragment.java | 31 ++++++++++++++++--- core/java/android/app/FragmentManager.java | 5 +++ .../java/android/app/FragmentTransaction.java | 16 ++++++++++ 7 files changed, 77 insertions(+), 5 deletions(-) diff --git a/api/current.txt b/api/current.txt index 0ca9875ead826..3506807e04bc7 100644 --- a/api/current.txt +++ b/api/current.txt @@ -4516,6 +4516,7 @@ package android.app { method public final boolean isInLayout(); method public final boolean isRemoving(); method public final boolean isResumed(); + method public final boolean isStateSaved(); method public final boolean isVisible(); method public void onActivityCreated(android.os.Bundle); method public void onActivityResult(int, int, android.content.Intent); @@ -4754,6 +4755,7 @@ package android.app { method public abstract android.app.FragmentTransaction hide(android.app.Fragment); method public abstract boolean isAddToBackStackAllowed(); method public abstract boolean isEmpty(); + method public abstract android.app.FragmentTransaction postOnCommit(java.lang.Runnable); method public abstract android.app.FragmentTransaction remove(android.app.Fragment); method public abstract android.app.FragmentTransaction replace(int, android.app.Fragment); method public abstract android.app.FragmentTransaction replace(int, android.app.Fragment, java.lang.String); diff --git a/api/system-current.txt b/api/system-current.txt index 13112403f13cb..d2cd901050a6c 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -4676,6 +4676,7 @@ package android.app { method public final boolean isInLayout(); method public final boolean isRemoving(); method public final boolean isResumed(); + method public final boolean isStateSaved(); method public final boolean isVisible(); method public void onActivityCreated(android.os.Bundle); method public void onActivityResult(int, int, android.content.Intent); @@ -4914,6 +4915,7 @@ package android.app { method public abstract android.app.FragmentTransaction hide(android.app.Fragment); method public abstract boolean isAddToBackStackAllowed(); method public abstract boolean isEmpty(); + method public abstract android.app.FragmentTransaction postOnCommit(java.lang.Runnable); method public abstract android.app.FragmentTransaction remove(android.app.Fragment); method public abstract android.app.FragmentTransaction replace(int, android.app.Fragment); method public abstract android.app.FragmentTransaction replace(int, android.app.Fragment, java.lang.String); diff --git a/api/test-current.txt b/api/test-current.txt index 02616298a2416..75a040612aa0e 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -4526,6 +4526,7 @@ package android.app { method public final boolean isInLayout(); method public final boolean isRemoving(); method public final boolean isResumed(); + method public final boolean isStateSaved(); method public final boolean isVisible(); method public void onActivityCreated(android.os.Bundle); method public void onActivityResult(int, int, android.content.Intent); @@ -4764,6 +4765,7 @@ package android.app { method public abstract android.app.FragmentTransaction hide(android.app.Fragment); method public abstract boolean isAddToBackStackAllowed(); method public abstract boolean isEmpty(); + method public abstract android.app.FragmentTransaction postOnCommit(java.lang.Runnable); method public abstract android.app.FragmentTransaction remove(android.app.Fragment); method public abstract android.app.FragmentTransaction replace(int, android.app.Fragment); method public abstract android.app.FragmentTransaction replace(int, android.app.Fragment, java.lang.String); diff --git a/core/java/android/app/BackStackRecord.java b/core/java/android/app/BackStackRecord.java index a27775ea4d851..66b2355dcb6e6 100644 --- a/core/java/android/app/BackStackRecord.java +++ b/core/java/android/app/BackStackRecord.java @@ -220,6 +220,8 @@ final class BackStackRecord extends FragmentTransaction implements int mIndex = -1; boolean mAllowOptimization; + ArrayList mCommitRunnables; + int mBreadCrumbTitleRes; CharSequence mBreadCrumbTitleText; int mBreadCrumbShortTitleRes; @@ -620,6 +622,28 @@ final class BackStackRecord extends FragmentTransaction implements } } + @Override + public FragmentTransaction postOnCommit(Runnable runnable) { + if (runnable == null) { + throw new IllegalArgumentException("runnable cannot be null"); + } + disallowAddToBackStack(); + if (mCommitRunnables == null) { + mCommitRunnables = new ArrayList<>(); + } + mCommitRunnables.add(runnable); + return this; + } + + public void runOnCommitRunnables() { + if (mCommitRunnables != null) { + for (int i = 0, N = mCommitRunnables.size(); i < N; i++) { + mCommitRunnables.get(i).run(); + } + mCommitRunnables = null; + } + } + public int commit() { return commitInternal(false); } diff --git a/core/java/android/app/Fragment.java b/core/java/android/app/Fragment.java index 612998dd51583..108ab71923af2 100644 --- a/core/java/android/app/Fragment.java +++ b/core/java/android/app/Fragment.java @@ -714,14 +714,20 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene } /** - * Supply the construction arguments for this fragment. This can only - * be called before the fragment has been attached to its activity; that - * is, you should call it immediately after constructing the fragment. The - * arguments supplied here will be retained across fragment destroy and + * Supply the construction arguments for this fragment. + * The arguments supplied here will be retained across fragment destroy and * creation. + * + *

This method cannot be called if the fragment is added to a FragmentManager and + * if {@link #isStateSaved()} would return true. Prior to {@link Build.VERSION_CODES#O}, + * this method may only be called if the fragment has not yet been added to a FragmentManager. + *

*/ public void setArguments(Bundle args) { - if (mIndex >= 0) { + // The isStateSaved requirement below was only added in Android O and is compatible + // because it loosens previous requirements rather than making them more strict. + // See method javadoc. + if (mIndex >= 0 && isStateSaved()) { throw new IllegalStateException("Fragment already active"); } mArguments = args; @@ -734,6 +740,21 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene return mArguments; } + /** + * Returns true if this fragment is added and its state has already been saved + * by its host. Any operations that would change saved state should not be performed + * if this method returns true, and some operations such as {@link #setArguments(Bundle)} + * will fail. + * + * @return true if this fragment's state has already been saved by its host + */ + public final boolean isStateSaved() { + if (mFragmentManager == null) { + return false; + } + return mFragmentManager.isStateSaved(); + } + /** * Set the initial saved state that this Fragment should restore itself * from when first being constructed, as returned by diff --git a/core/java/android/app/FragmentManager.java b/core/java/android/app/FragmentManager.java index b0150bdf19360..4f68ec71c5ddb 100644 --- a/core/java/android/app/FragmentManager.java +++ b/core/java/android/app/FragmentManager.java @@ -1787,6 +1787,10 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate } } + public boolean isStateSaved() { + return mStateSaved; + } + /** * Adds an action to the queue of pending actions. * @@ -2108,6 +2112,7 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate freeBackStackIndex(record.mIndex); record.mIndex = -1; } + record.runOnCommitRunnables(); } if (addToBackStack) { diff --git a/core/java/android/app/FragmentTransaction.java b/core/java/android/app/FragmentTransaction.java index 07a313c9eeaf6..c938aa607e105 100644 --- a/core/java/android/app/FragmentTransaction.java +++ b/core/java/android/app/FragmentTransaction.java @@ -303,6 +303,22 @@ public abstract class FragmentTransaction { */ public abstract FragmentTransaction setAllowOptimization(boolean allowOptimization); + /** + * Add a Runnable to this transaction that will be run after this transaction has + * been committed. If fragment transactions are {@link #setAllowOptimization(boolean) optimized} + * this may be after other subsequent fragment operations have also taken place, or operations + * in this transaction may have been optimized out due to the presence of a subsequent + * fragment transaction in the batch. + * + *

postOnCommit may not be used with transactions + * {@link #addToBackStack(String) added to the back stack} as Runnables cannot be persisted + * with back stack state.

+ * + * @param runnable Runnable to add + * @return this FragmentTransaction + */ + public abstract FragmentTransaction postOnCommit(Runnable runnable); + /** * Schedules a commit of this transaction. The commit does * not happen immediately; it will be scheduled as work on the main thread