Add API to identify Fragment from view

Currently there is no way to identify if a view is associated with a
Fragment, or what Fragment the view may be associated with.

This changed adds a static API to FragmentManager to allow a Fragment to
be found from a view. The fragment is stored in the view as a tag with
a static id after the fragment view is created, but before the view is
added to the fragment container. The fragment can then be retrieved from
any view that is a child of the fragment's view.

Test: Added Tests in FragmentViewTest, ./gradlew checkApi
BUG: 136494650
Change-Id: Idd79a1bb21c1c62473d1498267ab8ddf5e27cda9
diff --git a/fragment/fragment-ktx/api/1.2.0-alpha02.txt b/fragment/fragment-ktx/api/1.2.0-alpha02.txt
index dd42d83..ab3b3b7 100644
--- a/fragment/fragment-ktx/api/1.2.0-alpha02.txt
+++ b/fragment/fragment-ktx/api/1.2.0-alpha02.txt
@@ -22,5 +22,10 @@
     method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<VM> viewModels(androidx.fragment.app.Fragment, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStoreOwner> ownerProducer = { this }, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer = null);
   }
 
+  public final class ViewKt {
+    ctor public ViewKt();
+    method public static <F extends androidx.fragment.app.Fragment> F findFragment(android.view.View);
+  }
+
 }
 
diff --git a/fragment/fragment-ktx/api/current.txt b/fragment/fragment-ktx/api/current.txt
index dd42d83..ab3b3b7 100644
--- a/fragment/fragment-ktx/api/current.txt
+++ b/fragment/fragment-ktx/api/current.txt
@@ -22,5 +22,10 @@
     method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<VM> viewModels(androidx.fragment.app.Fragment, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStoreOwner> ownerProducer = { this }, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer = null);
   }
 
+  public final class ViewKt {
+    ctor public ViewKt();
+    method public static <F extends androidx.fragment.app.Fragment> F findFragment(android.view.View);
+  }
+
 }
 
diff --git a/fragment/fragment-ktx/api/restricted_1.2.0-alpha02.txt b/fragment/fragment-ktx/api/restricted_1.2.0-alpha02.txt
index dd42d83..ab3b3b7 100644
--- a/fragment/fragment-ktx/api/restricted_1.2.0-alpha02.txt
+++ b/fragment/fragment-ktx/api/restricted_1.2.0-alpha02.txt
@@ -22,5 +22,10 @@
     method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<VM> viewModels(androidx.fragment.app.Fragment, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStoreOwner> ownerProducer = { this }, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer = null);
   }
 
+  public final class ViewKt {
+    ctor public ViewKt();
+    method public static <F extends androidx.fragment.app.Fragment> F findFragment(android.view.View);
+  }
+
 }
 
diff --git a/fragment/fragment-ktx/api/restricted_current.txt b/fragment/fragment-ktx/api/restricted_current.txt
index dd42d83..ab3b3b7 100644
--- a/fragment/fragment-ktx/api/restricted_current.txt
+++ b/fragment/fragment-ktx/api/restricted_current.txt
@@ -22,5 +22,10 @@
     method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<VM> viewModels(androidx.fragment.app.Fragment, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStoreOwner> ownerProducer = { this }, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer = null);
   }
 
+  public final class ViewKt {
+    ctor public ViewKt();
+    method public static <F extends androidx.fragment.app.Fragment> F findFragment(android.view.View);
+  }
+
 }
 
diff --git a/fragment/fragment-ktx/src/androidTest/java/androidx/fragment/app/ViewTest.kt b/fragment/fragment-ktx/src/androidTest/java/androidx/fragment/app/ViewTest.kt
new file mode 100644
index 0000000..d1eb53a
--- /dev/null
+++ b/fragment/fragment-ktx/src/androidTest/java/androidx/fragment/app/ViewTest.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.content.Context
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.test.annotation.UiThreadTest
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+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
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ViewTest {
+    @get:Rule val activityRule = ActivityTestRule<TestActivity>(TestActivity::class.java)
+    private val fragmentManager get() = activityRule.activity.supportFragmentManager
+
+    @UiThreadTest
+    @Test
+    fun findFragment() {
+        val fragment = ViewFragment()
+        fragmentManager.commitNow {
+            add(android.R.id.content, fragment)
+        }
+
+        val foundFragment = fragment.requireView().findFragment<ViewFragment>()
+        assertWithMessage("View should have Fragment set")
+            .that(foundFragment)
+            .isSameInstanceAs(fragment)
+    }
+
+    @Test
+    fun findFragmentNull() {
+        val view = View(ApplicationProvider.getApplicationContext() as Context)
+        try {
+            view.findFragment<Fragment>()
+            fail("findFragment should throw IllegalStateException if a Fragment was not set")
+        } catch (e: IllegalStateException) {
+            assertThat(e)
+                .hasMessageThat().contains("View $view does not have a Fragment set")
+        }
+    }
+}
+
+class ViewFragment : Fragment() {
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        return View(context)
+    }
+}
diff --git a/fragment/fragment-ktx/src/main/java/androidx/fragment/app/View.kt b/fragment/fragment-ktx/src/main/java/androidx/fragment/app/View.kt
new file mode 100644
index 0000000..63ae98f
--- /dev/null
+++ b/fragment/fragment-ktx/src/main/java/androidx/fragment/app/View.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.view.View
+
+/**
+ * Find a [Fragment] associated with a [View].
+ *
+ * This method will locate the [Fragment] associated with this view. This is automatically
+ * populated for the View returned by [Fragment.onCreateView] and its children.
+ *
+ * Calling this on a View that does not have a Fragment set will result in an
+ * [IllegalStateException]
+ */
+fun <F : Fragment> View.findFragment(): F = FragmentManager.findFragment(this)
diff --git a/fragment/fragment/api/1.2.0-alpha02.txt b/fragment/fragment/api/1.2.0-alpha02.txt
index 1bebde6..346d9b7 100644
--- a/fragment/fragment/api/1.2.0-alpha02.txt
+++ b/fragment/fragment/api/1.2.0-alpha02.txt
@@ -269,6 +269,7 @@
     method public void dump(String, java.io.FileDescriptor?, java.io.PrintWriter, String![]?);
     method public static void enableDebugLogging(boolean);
     method public boolean executePendingTransactions();
+    method public static <F extends androidx.fragment.app.Fragment> F findFragment(android.view.View);
     method public androidx.fragment.app.Fragment? findFragmentById(@IdRes int);
     method public androidx.fragment.app.Fragment? findFragmentByTag(String?);
     method public androidx.fragment.app.FragmentManager.BackStackEntry getBackStackEntryAt(int);
diff --git a/fragment/fragment/api/current.txt b/fragment/fragment/api/current.txt
index 1bebde6..346d9b7 100644
--- a/fragment/fragment/api/current.txt
+++ b/fragment/fragment/api/current.txt
@@ -269,6 +269,7 @@
     method public void dump(String, java.io.FileDescriptor?, java.io.PrintWriter, String![]?);
     method public static void enableDebugLogging(boolean);
     method public boolean executePendingTransactions();
+    method public static <F extends androidx.fragment.app.Fragment> F findFragment(android.view.View);
     method public androidx.fragment.app.Fragment? findFragmentById(@IdRes int);
     method public androidx.fragment.app.Fragment? findFragmentByTag(String?);
     method public androidx.fragment.app.FragmentManager.BackStackEntry getBackStackEntryAt(int);
diff --git a/fragment/fragment/api/restricted_1.2.0-alpha02.txt b/fragment/fragment/api/restricted_1.2.0-alpha02.txt
index a0aa372..7b475f2 100644
--- a/fragment/fragment/api/restricted_1.2.0-alpha02.txt
+++ b/fragment/fragment/api/restricted_1.2.0-alpha02.txt
@@ -274,6 +274,7 @@
     method public void dump(String, java.io.FileDescriptor?, java.io.PrintWriter, String![]?);
     method public static void enableDebugLogging(boolean);
     method public boolean executePendingTransactions();
+    method public static <F extends androidx.fragment.app.Fragment> F findFragment(android.view.View);
     method public androidx.fragment.app.Fragment? findFragmentById(@IdRes int);
     method public androidx.fragment.app.Fragment? findFragmentByTag(String?);
     method public androidx.fragment.app.FragmentManager.BackStackEntry getBackStackEntryAt(int);
diff --git a/fragment/fragment/api/restricted_current.txt b/fragment/fragment/api/restricted_current.txt
index a0aa372..7b475f2 100644
--- a/fragment/fragment/api/restricted_current.txt
+++ b/fragment/fragment/api/restricted_current.txt
@@ -274,6 +274,7 @@
     method public void dump(String, java.io.FileDescriptor?, java.io.PrintWriter, String![]?);
     method public static void enableDebugLogging(boolean);
     method public boolean executePendingTransactions();
+    method public static <F extends androidx.fragment.app.Fragment> F findFragment(android.view.View);
     method public androidx.fragment.app.Fragment? findFragmentById(@IdRes int);
     method public androidx.fragment.app.Fragment? findFragmentByTag(String?);
     method public androidx.fragment.app.FragmentManager.BackStackEntry getBackStackEntryAt(int);
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewTest.kt
index 70e0dfb..e65aa30 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewTest.kt
@@ -310,6 +310,46 @@
             .isEqualTo(Lifecycle.State.INITIALIZED)
     }
 
+    @Test
+    fun findFragmentNoTagSet() {
+        val view = View(activityRule.activity)
+        try {
+            FragmentManager.findFragment<Fragment>(view)
+            fail("findFragment should throw IllegalStateException if a Fragment was not set")
+        } catch (e: IllegalStateException) {
+            assertThat(e)
+                .hasMessageThat().contains("View $view does not have a Fragment set")
+        }
+    }
+
+    @Test
+    fun findFragmentAfterAdd() {
+        activityRule.setContentView(R.layout.simple_container)
+        val fm = activityRule.activity.supportFragmentManager
+
+        val fragment = StrictViewFragment()
+        fm.beginTransaction().add(R.id.fragmentContainer, fragment).commit()
+        activityRule.executePendingTransactions()
+
+        assertThat(FragmentManager.findFragment<StrictViewFragment>(fragment.requireView()))
+            .isSameInstanceAs(fragment)
+    }
+
+    @Test
+    fun findFragmentFindByIdChildView() {
+        activityRule.setContentView(R.layout.simple_container)
+        val fm = activityRule.activity.supportFragmentManager
+
+        val fragment = StrictViewFragment(R.layout.fragment_a)
+        fm.beginTransaction().add(R.id.fragmentContainer, fragment).commit()
+        activityRule.executePendingTransactions()
+
+        val view = fragment.requireView().findViewById<View>(R.id.textA)
+        assertThat(view).isNotNull()
+        assertThat(FragmentManager.findFragment<StrictViewFragment>(view))
+            .isSameInstanceAs(fragment)
+    }
+
     // Hide a fragment and its View should be GONE. Then pop it and the View should be VISIBLE
     @Test
     fun hideFragment() {
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 e76789e..c1354c8 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
@@ -36,6 +36,7 @@
 import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewParent;
 import android.view.animation.Animation;
 import android.view.animation.AnimationSet;
 import android.view.animation.AnimationUtils;
@@ -753,6 +754,55 @@
     }
 
     /**
+     * Find a {@link Fragment} associated with the given {@link View}.
+     *
+     * This method will locate the {@link Fragment} associated with this view. This is automatically
+     * populated for the View returned by {@link Fragment#onCreateView} and its children.
+     *
+     * @param view the view to search from
+     * @return the locally scoped {@link Fragment} to the given view
+     * @throws IllegalStateException if the given view does not correspond with a
+     * {@link Fragment}.
+     */
+    @NonNull
+    @SuppressWarnings("unchecked") // We should throw a ClassCast exception if the type is wrong
+    public static <F extends Fragment> F findFragment(@NonNull View view) {
+        Fragment fragment = findViewFragment(view);
+        if (fragment == null) {
+            throw new IllegalStateException("View " + view + " does not have a Fragment set");
+        }
+        return (F) fragment;
+    }
+
+    /**
+     * Recurse up the view hierarchy, looking for the Fragment
+     * @param view the view to search from
+     * @return the locally scoped {@link Fragment} to the given view, if found
+     */
+    @Nullable
+    static Fragment findViewFragment(@NonNull View view) {
+        while (view != null) {
+            Object tag = view.getTag(R.id.fragment_container_view_tag);
+            if (tag instanceof Fragment) {
+                return (Fragment) tag;
+            }
+            ViewParent parent = view.getParent();
+            view = parent instanceof View ? (View) parent : null;
+        }
+        return null;
+    }
+
+    /**
+     * Used to store the Fragment inside of its view's tag. This is done after the fragment's view
+     * is created, but before the view is added to the container.
+     *
+     * @param fragment The fragment to be set as a tag on its view
+     */
+    void setViewTag(@NonNull Fragment fragment) {
+        fragment.mView.setTag(R.id.fragment_container_view_tag, fragment);
+    }
+
+    /**
      * Get a list of all fragments that are currently added to the FragmentManager.
      * This may include those that are hidden as well as those that are shown.
      * This will not include any fragments only in the back stack, or fragments that
@@ -1259,6 +1309,7 @@
                             if (f.mView != null) {
                                 f.mInnerView = f.mView;
                                 f.mView.setSaveFromParentEnabled(false);
+                                setViewTag(f);
                                 if (container != null) {
                                     container.addView(f.mView);
                                 }
diff --git a/fragment/fragment/src/main/res/values/ids.xml b/fragment/fragment/src/main/res/values/ids.xml
new file mode 100644
index 0000000..db4ce6e
--- /dev/null
+++ b/fragment/fragment/src/main/res/values/ids.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.
+  -->
+
+<resources>
+        <item type="id" name="fragment_container_view_tag" />
+</resources>
\ No newline at end of file