Add Fragment#getViewLifecycleOwner

The Fragment's View lifecycle can diverge
from the Fragment's lifecycle in cases of
detached Fragments. This can cause issues with
LiveData where old observers should be cleared
when the View is destroyed to prevent duplication
with new observers created in onCreateView/onViewCreated.

By exposing a separate LifecycleOwner specifically for
the Fragment's View, developers can use that in place
of the Fragment itself to better model the Lifecycle
they actually care about.

Also adds a getViewLifecycleOwnerLiveData() for
observing changes in the View LifecycleOwner (i.e.,
creation, destruction, and recreation).

Test: new FragmentViewLifecycleTest passes
BUG: 72411063
Change-Id: I3f1531e64d4f18aed1ed69434029ab9b317e3886
diff --git a/fragment/api/current.txt b/fragment/api/current.txt
index 1ee47e3..89730f0 100644
--- a/fragment/api/current.txt
+++ b/fragment/api/current.txt
@@ -56,6 +56,8 @@
     method public final java.lang.CharSequence getText(int);
     method public boolean getUserVisibleHint();
     method public android.view.View getView();
+    method public androidx.lifecycle.LifecycleOwner getViewLifecycleOwner();
+    method public androidx.lifecycle.LiveData<androidx.lifecycle.LifecycleOwner> getViewLifecycleOwnerLiveData();
     method public androidx.lifecycle.ViewModelStore getViewModelStore();
     method public final int hashCode();
     method public static androidx.fragment.app.Fragment instantiate(android.content.Context, java.lang.String);
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleTest.java b/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleTest.java
new file mode 100644
index 0000000..32861f9
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleTest.java
@@ -0,0 +1,281 @@
+/*
+ * 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.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.os.Bundle;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+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.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Observer;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class FragmentViewLifecycleTest {
+
+    @Rule
+    public ActivityTestRule<FragmentTestActivity> mActivityRule =
+            new ActivityTestRule<>(FragmentTestActivity.class);
+
+    @Test
+    @UiThreadTest
+    public void testFragmentViewLifecycle() {
+        final FragmentTestActivity activity = mActivityRule.getActivity();
+        final FragmentManager fm = activity.getSupportFragmentManager();
+
+        final StrictViewFragment fragment = new StrictViewFragment();
+        fragment.setLayoutId(R.layout.fragment_a);
+        fm.beginTransaction().add(R.id.content, fragment).commitNow();
+        assertEquals(Lifecycle.State.RESUMED,
+                fragment.getViewLifecycleOwner().getLifecycle().getCurrentState());
+    }
+
+    @Test
+    @UiThreadTest
+    public void testFragmentViewLifecycleNullView() {
+        final FragmentTestActivity activity = mActivityRule.getActivity();
+        final FragmentManager fm = activity.getSupportFragmentManager();
+
+        final Fragment fragment = new Fragment();
+        fm.beginTransaction().add(fragment, "fragment").commitNow();
+        try {
+            fragment.getViewLifecycleOwner();
+            fail("getViewLifecycleOwner should be unavailable if onCreateView returned null");
+        } catch (IllegalStateException expected) {
+            // Expected
+        }
+    }
+
+    @Test
+    @UiThreadTest
+    public void testObserveInOnCreateViewNullView() {
+        final FragmentTestActivity activity = mActivityRule.getActivity();
+        final FragmentManager fm = activity.getSupportFragmentManager();
+
+        final Fragment fragment = new ObserveInOnCreateViewFragment();
+        try {
+            fm.beginTransaction().add(fragment, "fragment").commitNow();
+            fail("Fragments accessing view lifecycle should fail if onCreateView returned null");
+        } catch (IllegalStateException expected) {
+            // We need to clean up the Fragment to avoid it still being around
+            // when the instrumentation test Activity pauses. Real apps would have
+            // just crashed right after onCreateView().
+            fm.beginTransaction().remove(fragment).commitNow();
+        }
+    }
+
+    @Test
+    public void testFragmentViewLifecycleRunOnCommit() throws Throwable {
+        final FragmentTestActivity activity = mActivityRule.getActivity();
+        final FragmentManager fm = activity.getSupportFragmentManager();
+
+        final CountDownLatch countDownLatch = new CountDownLatch(1);
+        final StrictViewFragment fragment = new StrictViewFragment();
+        fragment.setLayoutId(R.layout.fragment_a);
+        fm.beginTransaction().add(R.id.content, fragment).runOnCommit(new Runnable() {
+            @Override
+            public void run() {
+                assertEquals(Lifecycle.State.RESUMED,
+                        fragment.getViewLifecycleOwner().getLifecycle().getCurrentState());
+                countDownLatch.countDown();
+
+            }
+        }).commit();
+        countDownLatch.await(1, TimeUnit.SECONDS);
+    }
+
+    @Test
+    public void testFragmentViewLifecycleOwnerLiveData() throws Throwable {
+        final FragmentTestActivity activity = mActivityRule.getActivity();
+        final FragmentManager fm = activity.getSupportFragmentManager();
+
+        final CountDownLatch countDownLatch = new CountDownLatch(2);
+        final StrictViewFragment fragment = new StrictViewFragment();
+        fragment.setLayoutId(R.layout.fragment_a);
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                fragment.getViewLifecycleOwnerLiveData().observe(activity,
+                        new Observer<LifecycleOwner>() {
+                            @Override
+                            public void onChanged(LifecycleOwner lifecycleOwner) {
+                                if (lifecycleOwner != null) {
+                                    assertTrue("Fragment View LifecycleOwner should be "
+                                                    + "only be set after  onCreateView()",
+                                            fragment.mOnCreateViewCalled);
+                                    countDownLatch.countDown();
+                                } else {
+                                    assertTrue("Fragment View LifecycleOwner should be "
+                                            + "set to null after onDestroyView()",
+                                            fragment.mOnDestroyViewCalled);
+                                    countDownLatch.countDown();
+                                }
+                            }
+                        });
+                fm.beginTransaction().add(R.id.content, fragment).commitNow();
+                // Now remove the Fragment to trigger the destruction of the view
+                fm.beginTransaction().remove(fragment).commitNow();
+            }
+        });
+        countDownLatch.await(1, TimeUnit.SECONDS);
+    }
+
+    @Test
+    @UiThreadTest
+    public void testFragmentViewLifecycleDetach() {
+        final FragmentTestActivity activity = mActivityRule.getActivity();
+        final FragmentManager fm = activity.getSupportFragmentManager();
+
+        final ObservingFragment fragment = new ObservingFragment();
+        fragment.setLayoutId(R.layout.fragment_a);
+        fm.beginTransaction().add(R.id.content, fragment).commitNow();
+        LifecycleOwner viewLifecycleOwner = fragment.getViewLifecycleOwner();
+        assertEquals(Lifecycle.State.RESUMED,
+                viewLifecycleOwner.getLifecycle().getCurrentState());
+        assertTrue("LiveData should have active observers when RESUMED",
+                fragment.mLiveData.hasActiveObservers());
+
+        fm.beginTransaction().detach(fragment).commitNow();
+        assertEquals(Lifecycle.State.DESTROYED,
+                viewLifecycleOwner.getLifecycle().getCurrentState());
+        assertFalse("LiveData should not have active observers after detach()",
+                fragment.mLiveData.hasActiveObservers());
+        try {
+            fragment.getViewLifecycleOwner();
+            fail("getViewLifecycleOwner should be unavailable after onDestroyView");
+        } catch (IllegalStateException expected) {
+            // Expected
+        }
+    }
+
+    @Test
+    @UiThreadTest
+    public void testFragmentViewLifecycleReattach() {
+        final FragmentTestActivity activity = mActivityRule.getActivity();
+        final FragmentManager fm = activity.getSupportFragmentManager();
+
+        final ObservingFragment fragment = new ObservingFragment();
+        fragment.setLayoutId(R.layout.fragment_a);
+        fm.beginTransaction().add(R.id.content, fragment).commitNow();
+        LifecycleOwner viewLifecycleOwner = fragment.getViewLifecycleOwner();
+        assertEquals(Lifecycle.State.RESUMED,
+                viewLifecycleOwner.getLifecycle().getCurrentState());
+        assertTrue("LiveData should have active observers when RESUMED",
+                fragment.mLiveData.hasActiveObservers());
+
+        fm.beginTransaction().detach(fragment).commitNow();
+        // The existing view lifecycle should be destroyed
+        assertEquals(Lifecycle.State.DESTROYED,
+                viewLifecycleOwner.getLifecycle().getCurrentState());
+        assertFalse("LiveData should not have active observers after detach()",
+                fragment.mLiveData.hasActiveObservers());
+
+        fm.beginTransaction().attach(fragment).commitNow();
+        assertNotEquals("A new view LifecycleOwner should be returned after reattachment",
+                viewLifecycleOwner, fragment.getViewLifecycleOwner());
+        assertEquals(Lifecycle.State.RESUMED,
+                fragment.getViewLifecycleOwner().getLifecycle().getCurrentState());
+        assertTrue("LiveData should have active observers when RESUMED",
+                fragment.mLiveData.hasActiveObservers());
+    }
+
+    public static class ObserveInOnCreateViewFragment extends Fragment {
+        MutableLiveData<Boolean> mLiveData = new MutableLiveData<>();
+        private Observer<Boolean> mOnCreateViewObserver = new Observer<Boolean>() {
+            @Override
+            public void onChanged(Boolean value) {
+            }
+        };
+
+        @Override
+        public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                Bundle savedInstanceState) {
+            mLiveData.observe(getViewLifecycleOwner(), mOnCreateViewObserver);
+            assertTrue("LiveData should have observers after onCreateView observe",
+                    mLiveData.hasObservers());
+            // Return null - oops!
+            return null;
+        }
+
+    }
+
+    public static class ObservingFragment extends StrictViewFragment {
+        MutableLiveData<Boolean> mLiveData = new MutableLiveData<>();
+        private Observer<Boolean> mOnCreateViewObserver = new Observer<Boolean>() {
+            @Override
+            public void onChanged(Boolean value) {
+            }
+        };
+        private Observer<Boolean> mOnViewCreatedObserver = new Observer<Boolean>() {
+            @Override
+            public void onChanged(Boolean value) {
+            }
+        };
+        private Observer<Boolean> mOnViewStateRestoredObserver = new Observer<Boolean>() {
+            @Override
+            public void onChanged(Boolean value) {
+            }
+        };
+
+        @Override
+        public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                Bundle savedInstanceState) {
+            mLiveData.observe(getViewLifecycleOwner(), mOnCreateViewObserver);
+            assertTrue("LiveData should have observers after onCreateView observe",
+                    mLiveData.hasObservers());
+            return super.onCreateView(inflater, container, savedInstanceState);
+        }
+
+        @Override
+        public void onViewCreated(View view, Bundle savedInstanceState) {
+            super.onViewCreated(view, savedInstanceState);
+            mLiveData.observe(getViewLifecycleOwner(), mOnViewCreatedObserver);
+            assertTrue("LiveData should have observers after onViewCreated observe",
+                    mLiveData.hasObservers());
+        }
+
+        @Override
+        public void onViewStateRestored(Bundle savedInstanceState) {
+            super.onViewStateRestored(savedInstanceState);
+            mLiveData.observe(getViewLifecycleOwner(), mOnViewStateRestoredObserver);
+            assertTrue("LiveData should have observers after onViewStateRestored observe",
+                    mLiveData.hasObservers());
+        }
+    }
+}
diff --git a/fragment/src/main/java/androidx/fragment/app/Fragment.java b/fragment/src/main/java/androidx/fragment/app/Fragment.java
index fc34d9e..ea99ddc 100644
--- a/fragment/src/main/java/androidx/fragment/app/Fragment.java
+++ b/fragment/src/main/java/androidx/fragment/app/Fragment.java
@@ -45,6 +45,7 @@
 import android.widget.AdapterView;
 
 import androidx.annotation.CallSuper;
+import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
@@ -56,6 +57,8 @@
 import androidx.lifecycle.Lifecycle;
 import androidx.lifecycle.LifecycleOwner;
 import androidx.lifecycle.LifecycleRegistry;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
 import androidx.lifecycle.ViewModelStore;
 import androidx.lifecycle.ViewModelStoreOwner;
 import androidx.loader.app.LoaderManager;
@@ -244,11 +247,73 @@
 
     LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);
 
+    // These are initialized in performCreateView and unavailable outside of the
+    // onCreateView/onDestroyView lifecycle
+    private LifecycleRegistry mViewLifecycleRegistry;
+    LifecycleOwner mViewLifecycleOwner;
+    MutableLiveData<LifecycleOwner> mViewLifecycleOwnerLiveData = new MutableLiveData<>();
+
     @Override
     public Lifecycle getLifecycle() {
         return mLifecycleRegistry;
     }
 
+    /**
+     * Get a {@link LifecycleOwner} that represents the {@link #getView() Fragment's View}
+     * lifecycle. In most cases, this mirrors the lifecycle of the Fragment itself, but in cases
+     * of {@link FragmentTransaction#detach(Fragment) detached} Fragments, the lifecycle of the
+     * Fragment can be considerably longer than the lifecycle of the View itself.
+     * <p>
+     * Namely, the lifecycle of the Fragment's View is:
+     * <ol>
+     * <li>{@link Lifecycle.Event#ON_CREATE created} in {@link #onViewStateRestored(Bundle)}</li>
+     * <li>{@link Lifecycle.Event#ON_START started} in {@link #onStart()}</li>
+     * <li>{@link Lifecycle.Event#ON_RESUME resumed} in {@link #onResume()}</li>
+     * <li>{@link Lifecycle.Event#ON_PAUSE paused} in {@link #onPause()}</li>
+     * <li>{@link Lifecycle.Event#ON_STOP stopped} in {@link #onStop()}</li>
+     * <li>{@link Lifecycle.Event#ON_DESTROY destroyed} in {@link #onDestroyView()}</li>
+     * </ol>
+     *
+     * The first method where it is safe to access the view lifecycle is
+     * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} under the condition that you must
+     * return a non-null view (an IllegalStateException will be thrown if you access the view
+     * lifecycle but don't return a non-null view).
+     * <p>The view lifecycle remains valid through the call to {@link #onDestroyView()}, after which
+     * {@link #getView()} will return null, the view lifecycle will be destroyed, and this method
+     * will throw an IllegalStateException. Consider using
+     * {@link #getViewLifecycleOwnerLiveData()} or {@link FragmentTransaction#runOnCommit(Runnable)}
+     * to receive a callback for when the Fragment's view lifecycle is available.
+     * <p>
+     * This should only be called on the main thread.
+     *
+     * @return A {@link LifecycleOwner} that represents the {@link #getView() Fragment's View}
+     * lifecycle.
+     * @throws IllegalStateException if the {@link #getView() Fragment's View is null}.
+     */
+    @MainThread
+    @NonNull
+    public LifecycleOwner getViewLifecycleOwner() {
+        if (mViewLifecycleOwner == null) {
+            throw new IllegalStateException("Can't access the Fragment View's LifecycleOwner when "
+                    + "getView() is null i.e., before onCreateView() or after onDestroyView()");
+        }
+        return mViewLifecycleOwner;
+    }
+
+    /**
+     * Retrieve a {@link LiveData} which allows you to observe the
+     * {@link #getViewLifecycleOwner() lifecycle of the Fragment's View}.
+     * <p>
+     * This will be set to the new {@link LifecycleOwner} after {@link #onCreateView} returns a
+     * non-null View and will set to null after {@link #onDestroyView()}.
+     *
+     * @return A LiveData that changes in sync with {@link #getViewLifecycleOwner()}.
+     */
+    @NonNull
+    public LiveData<LifecycleOwner> getViewLifecycleOwnerLiveData() {
+        return mViewLifecycleOwnerLiveData;
+    }
+
     @NonNull
     @Override
     public ViewModelStore getViewModelStore() {
@@ -1529,6 +1594,9 @@
     @CallSuper
     public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
         mCalled = true;
+        if (mView != null) {
+            mViewLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
+        }
     }
 
     /**
@@ -1637,6 +1705,9 @@
     @CallSuper
     public void onDestroyView() {
         mCalled = true;
+        if (mView != null) {
+            mViewLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
+        }
     }
 
     /**
@@ -2336,13 +2407,35 @@
         mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
     }
 
-    View performCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+    void performCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
             @Nullable Bundle savedInstanceState) {
         if (mChildFragmentManager != null) {
             mChildFragmentManager.noteStateNotSaved();
         }
         mPerformedCreateView = true;
-        return onCreateView(inflater, container, savedInstanceState);
+        mViewLifecycleOwner = new LifecycleOwner() {
+            @Override
+            public Lifecycle getLifecycle() {
+                if (mViewLifecycleRegistry == null) {
+                    mViewLifecycleRegistry = new LifecycleRegistry(mViewLifecycleOwner);
+                }
+                return mViewLifecycleRegistry;
+            }
+        };
+        mViewLifecycleRegistry = null;
+        mView = onCreateView(inflater, container, savedInstanceState);
+        if (mView != null) {
+            // Initialize the LifecycleRegistry if needed
+            mViewLifecycleOwner.getLifecycle();
+            // Then inform any Observers of the new LifecycleOwner
+            mViewLifecycleOwnerLiveData.setValue(mViewLifecycleOwner);
+        } else {
+            if (mViewLifecycleRegistry != null) {
+                throw new IllegalStateException("Called getViewLifecycleOwner() but "
+                        + "onCreateView() returned null");
+            }
+            mViewLifecycleOwner = null;
+        }
     }
 
     void performActivityCreated(Bundle savedInstanceState) {
@@ -2377,6 +2470,9 @@
             mChildFragmentManager.dispatchStart();
         }
         mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+        if (mView != null) {
+            mViewLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+        }
     }
 
     void performResume() {
@@ -2396,6 +2492,9 @@
             mChildFragmentManager.execPendingActions();
         }
         mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);
+        if (mView != null) {
+            mViewLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);
+        }
     }
 
     void noteStateNotSaved() {
@@ -2521,6 +2620,9 @@
     }
 
     void performPause() {
+        if (mView != null) {
+            mViewLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE);
+        }
         mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE);
         if (mChildFragmentManager != null) {
             mChildFragmentManager.dispatchPause();
diff --git a/fragment/src/main/java/androidx/fragment/app/FragmentManager.java b/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
index f6fc2b8..9d637c4 100644
--- a/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
+++ b/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
@@ -1457,7 +1457,7 @@
                                 }
                             }
                             f.mContainer = container;
-                            f.mView = f.performCreateView(f.performGetLayoutInflater(
+                            f.performCreateView(f.performGetLayoutInflater(
                                     f.mSavedFragmentState), container, f.mSavedFragmentState);
                             if (f.mView != null) {
                                 f.mInnerView = f.mView;
@@ -1562,6 +1562,10 @@
                         }
                         f.mContainer = null;
                         f.mView = null;
+                        // Set here to ensure that Observers are called after
+                        // the Fragment's view is set to null
+                        f.mViewLifecycleOwner = null;
+                        f.mViewLifecycleOwnerLiveData.setValue(null);
                         f.mInnerView = null;
                         f.mInLayout = false;
                     }
@@ -1693,7 +1697,7 @@
 
     void ensureInflatedFragmentView(Fragment f) {
         if (f.mFromLayout && !f.mPerformedCreateView) {
-            f.mView = f.performCreateView(f.performGetLayoutInflater(
+            f.performCreateView(f.performGetLayoutInflater(
                     f.mSavedFragmentState), null, f.mSavedFragmentState);
             if (f.mView != null) {
                 f.mInnerView = f.mView;