Ensure enqueueAction throws if fragment manager has no host

Added checks to enqueueAction to throw IllegalStateException if the
FragmentManager has been destroyed or it was never attached to a host.

Test: all tests pass
BUG: 64896609
Change-Id: Ie24565059f2ee7a54fda4c5253a13b3006db0994
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.kt
index a7bb60c..bfb0022 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.kt
@@ -672,6 +672,62 @@
         }
     }
 
+    @Test
+    @UiThreadTest
+    fun popBackStackAfterManagerDestroyed() {
+        val viewModelStore = ViewModelStore()
+        val fc = activityRule.startupFragmentController(viewModelStore)
+        val fm = fc.supportFragmentManager
+
+        val fragment1 = StrictFragment()
+        fragment1.retainInstance = true
+        fm.beginTransaction()
+            .add(fragment1, "1")
+            .commitNow()
+
+        // Now destroy the Fragment Manager
+        fc.dispatchPause()
+        fc.dispatchStop()
+        fc.dispatchDestroy()
+
+        try {
+            fm.popBackStack()
+            fail("PopBackStack after FragmentManager destroyed should throw IllegalStateException")
+        } catch (e: IllegalStateException) {
+            assertWithMessage("popBackStack should throw an IllegalStateException")
+                .that(e)
+                .hasMessageThat()
+                .contains("FragmentManager has been destroyed")
+        }
+    }
+
+    @Test
+    @UiThreadTest
+    fun commitWhenFragmentManagerNeverAttached() {
+        val viewModelStore = ViewModelStore()
+        val fc = FragmentController.createController(
+            ControllerHostCallbacks(activityRule.activity, viewModelStore)
+        )
+        val fm = fc.supportFragmentManager
+
+        val fragment1 = StrictFragment()
+        fragment1.retainInstance = true
+
+        try {
+            fm.beginTransaction()
+                .add(fragment1, "1")
+                .commit()
+            fail("Commit when FragmentManager never attached should throw " +
+                    "IllegalStateException")
+        } catch (e: IllegalStateException) {
+            assertWithMessage("Commit when FragmentManager never attached should throw an " +
+                    "IllegalStateException")
+                .that(e)
+                .hasMessageThat()
+                .contains("FragmentManager has not been attached to a host.")
+        }
+    }
+
     /**
      * When a fragment is saved in non-config, it should be restored to the same index.
      */
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
index cab38bf..20bc876 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
@@ -2025,6 +2025,14 @@
      */
     void enqueueAction(OpGenerator action, boolean allowStateLoss) {
         if (!allowStateLoss) {
+            if (mHost == null) {
+                if (mDestroyed) {
+                    throw new IllegalStateException("FragmentManager has been destroyed");
+                } else {
+                    throw new IllegalStateException("FragmentManager has not been attached to a "
+                            + "host.");
+                }
+            }
             checkStateLoss();
         }
         synchronized (mPendingActions) {