From 691f2f9d0232fbcd7181f9991f2d0db211e75cb0 Mon Sep 17 00:00:00 2001
From: John Shelley <john.shelley.p@gmail.com>
Date: Thu, 4 Apr 2019 00:18:52 -0700
Subject: [PATCH] Android - Add a ReactFragment (#12199)

Summary:
React Native on Android has currently been focused and targeted at using an [Activity](https://developer.android.com/reference/android/app/Activity.html) for its main form of instantiation.

While this has probably worked for most companies and developers, you lose some of the modularity of a more cohesive application when working in a "brown-field" project that is currently native. This hurts more companies that are looking to adopt React Native and slowly implement it in a fully native application.

A lot of developers follow Android's guidelines of using Fragments in their projects, even if it is a debated subject in the Android community, and this addition will allow others to embrace React Native more freely. (I even assume it could help with managing navigation state in applications that contain a decent amount of Native code and would be appreciated in those projects. Such as sharing the Toolbar, TabBar, ViewPager, etc in Native Android)

Even with this addition, a developer will still need to host the fragment in an activity, but now that activity can contain native logic like a Drawer, Tabs, ViewPager, etc.

**Test plan (required)**
* We have been using this class at Hudl for over a couple of months and have found it valuable.
* If the community agrees on the addition, I can add documentation to the Android sections to include notes about the potential of this Fragment.
* If the community agrees on the addition, I can update one or more of the examples in the `/Examples` folder and make use of the Fragment, or even create a new example that uses a native layout manager like Drawer, Tabs, Viewpager, etc)

Make sure tests pass on both Travis and Circle CI.

_To Note:_
* There is also talk of using React Native inside Android Fragment's without any legit documentation, this could help remedy some of that with more documentation included in this PR https://facebook.github.io/react-native/releases/0.26/docs/embedded-app-android.html#sharing-a-reactinstance-across-multiple-activities-fragments-in-your-app
* Others have also requested something similar and have a half-baked solution as well http://stackoverflow.com/questions/35221447/react-native-inside-a-fragment

[ANDROID][FEATURE][ReactAndroid/src/main/java/com/facebook/react/ReactFragment.java] - Adds support for Android's Fragment system. This allows for a more hybrid application.
<!--
Help reviewers and the release process by writing your own release notes

**INTERNAL and MINOR tagged notes will not be included in the next version's final release notes.**

  CATEGORY
[----------]        TYPE
[ CLI      ]   [-------------]      LOCATION
[ DOCS     ]   [ BREAKING    ]   [-------------]
[ GENERAL  ]   [ BUGFIX      ]   [-{Component}-]
[ INTERNAL ]   [ ENHANCEMENT ]   [ {File}      ]
[ IOS      ]   [ FEATURE     ]   [ {Directory} ]   |-----------|
[ ANDROID  ]   [ MINOR       ]   [ {Framework} ] - | {Message} |
[----------]   [-------------]   [-------------]   |-----------|

[CATEGORY] [TYPE] [LOCATION] - MESSAGE

 EXAMPLES:

 [IOS] [BREAKING] [FlatList] - Change a thing that breaks other things
 [ANDROID] [BUGFIX] [TextInput] - Did a thing to TextInput
 [CLI] [FEATURE] [local-cli/info/info.js] - CLI easier to do things with
 [DOCS] [BUGFIX] [GettingStarted.md] - Accidentally a thing/word
 [GENERAL] [ENHANCEMENT] [Yoga] - Added new yoga thing/position
 [INTERNAL] [FEATURE] [./scripts] - Added thing to script that nobody will see
-->
Pull Request resolved: https://github.com/facebook/react-native/pull/12199

Differential Revision: D14590665

Pulled By: mdvacca

fbshipit-source-id: b50b708cde458f9634e0c14b3952fa32f9d82048
---
 .../facebook/react/ReactActivityDelegate.java |  72 ++----
 .../com/facebook/react/ReactDelegate.java     | 147 ++++++++++++
 .../com/facebook/react/ReactFragment.java     | 209 ++++++++++++++++++
 3 files changed, 373 insertions(+), 55 deletions(-)
 create mode 100644 ReactAndroid/src/main/java/com/facebook/react/ReactDelegate.java
 create mode 100644 ReactAndroid/src/main/java/com/facebook/react/ReactFragment.java

diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java b/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java
index 26a95665654d32..f0989641c7edc7 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java
@@ -15,10 +15,9 @@
 
 import com.facebook.infer.annotation.Assertions;
 import com.facebook.react.bridge.Callback;
-import com.facebook.react.devsupport.DoubleTapReloadRecognizer;
-import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
 import com.facebook.react.modules.core.PermissionListener;
 
+import com.facebook.react.uimanager.RootView;
 import javax.annotation.Nullable;
 
 /**
@@ -31,10 +30,10 @@ public class ReactActivityDelegate {
   private final @Nullable Activity mActivity;
   private final @Nullable String mMainComponentName;
 
-  private @Nullable ReactRootView mReactRootView;
-  private @Nullable DoubleTapReloadRecognizer mDoubleTapReloadRecognizer;
   private @Nullable PermissionListener mPermissionListener;
   private @Nullable Callback mPermissionsCallback;
+  private ReactDelegate mReactDelegate;
+
 
   @Deprecated
   public ReactActivityDelegate(Activity activity, @Nullable String mainComponentName) {
@@ -52,7 +51,7 @@ public ReactActivityDelegate(ReactActivity activity, @Nullable String mainCompon
   }
 
   protected ReactRootView createRootView() {
-    return new ReactRootView(getContext());
+    return mReactDelegate.createRootView();
   }
 
   /**
@@ -67,7 +66,7 @@ protected ReactNativeHost getReactNativeHost() {
   }
 
   public ReactInstanceManager getReactInstanceManager() {
-    return getReactNativeHost().getReactInstanceManager();
+    return mReactDelegate.getReactInstanceManager();
   }
 
   public String getMainComponentName() {
@@ -76,36 +75,24 @@ public String getMainComponentName() {
 
   protected void onCreate(Bundle savedInstanceState) {
     String mainComponentName = getMainComponentName();
-    if (mainComponentName != null) {
-      loadApp(mainComponentName);
+    mReactDelegate = new ReactDelegate(getPlainActivity(), getReactNativeHost(), mainComponentName, getLaunchOptions());
+    if (mMainComponentName != null) {
+      mReactDelegate.loadApp();
+      getPlainActivity().setContentView(mReactDelegate.getReactRootView());
     }
-    mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
   }
 
   protected void loadApp(String appKey) {
-    if (mReactRootView != null) {
-      throw new IllegalStateException("Cannot loadApp while app is already running.");
-    }
-    mReactRootView = createRootView();
-    mReactRootView.startReactApplication(
-      getReactNativeHost().getReactInstanceManager(),
-      appKey,
-      getLaunchOptions());
-    getPlainActivity().setContentView(mReactRootView);
+    mReactDelegate.loadApp(appKey);
+    getPlainActivity().setContentView(mReactDelegate.getReactRootView());
   }
 
   protected void onPause() {
-    if (getReactNativeHost().hasInstance()) {
-      getReactNativeHost().getReactInstanceManager().onHostPause(getPlainActivity());
-    }
+    mReactDelegate.onHostPause();
   }
 
   protected void onResume() {
-    if (getReactNativeHost().hasInstance()) {
-      getReactNativeHost().getReactInstanceManager().onHostResume(
-        getPlainActivity(),
-        (DefaultHardwareBackBtnHandler) getPlainActivity());
-    }
+    mReactDelegate.onHostResume();
 
     if (mPermissionsCallback != null) {
       mPermissionsCallback.invoke();
@@ -114,20 +101,11 @@ protected void onResume() {
   }
 
   protected void onDestroy() {
-    if (mReactRootView != null) {
-      mReactRootView.unmountReactApplication();
-      mReactRootView = null;
-    }
-    if (getReactNativeHost().hasInstance()) {
-      getReactNativeHost().getReactInstanceManager().onHostDestroy(getPlainActivity());
-    }
+    mReactDelegate.onHostDestroy();
   }
 
   public void onActivityResult(int requestCode, int resultCode, Intent data) {
-    if (getReactNativeHost().hasInstance()) {
-      getReactNativeHost().getReactInstanceManager()
-        .onActivityResult(getPlainActivity(), requestCode, resultCode, data);
-    }
+    mReactDelegate.onActivityResult(requestCode, resultCode, data, true);
   }
 
   public boolean onKeyDown(int keyCode, KeyEvent event) {
@@ -141,19 +119,7 @@ && getReactNativeHost().getUseDeveloperSupport()
   }
 
   public boolean onKeyUp(int keyCode, KeyEvent event) {
-    if (getReactNativeHost().hasInstance() && getReactNativeHost().getUseDeveloperSupport()) {
-      if (keyCode == KeyEvent.KEYCODE_MENU) {
-        getReactNativeHost().getReactInstanceManager().showDevOptionsDialog();
-        return true;
-      }
-      boolean didDoubleTapR = Assertions.assertNotNull(mDoubleTapReloadRecognizer)
-        .didDoubleTapR(keyCode, getPlainActivity().getCurrentFocus());
-      if (didDoubleTapR) {
-        getReactNativeHost().getReactInstanceManager().getDevSupportManager().handleReloadJS();
-        return true;
-      }
-    }
-    return false;
+    return mReactDelegate.shouldShowDevMenuOrReload(keyCode, event);
   }
 
   public boolean onKeyLongPress(int keyCode, KeyEvent event) {
@@ -167,11 +133,7 @@ && getReactNativeHost().getUseDeveloperSupport()
   }
 
   public boolean onBackPressed() {
-    if (getReactNativeHost().hasInstance()) {
-      getReactNativeHost().getReactInstanceManager().onBackPressed();
-      return true;
-    }
-    return false;
+    return mReactDelegate.onBackPressed();
   }
 
   public boolean onNewIntent(Intent intent) {
diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactDelegate.java b/ReactAndroid/src/main/java/com/facebook/react/ReactDelegate.java
new file mode 100644
index 00000000000000..e219c8be3c6626
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/ReactDelegate.java
@@ -0,0 +1,147 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.react;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.KeyEvent;
+
+import com.facebook.infer.annotation.Assertions;
+import com.facebook.react.devsupport.DoubleTapReloadRecognizer;
+import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
+
+import javax.annotation.Nullable;
+
+/**
+ * A delegate for handling React Application support. This delegate is unaware whether it is used in
+ * an {@link Activity} or a {@link android.app.Fragment}.
+ */
+public class ReactDelegate {
+
+  private final Activity mActivity;
+  private ReactRootView mReactRootView;
+
+  @Nullable
+  private final String mMainComponentName;
+
+  @Nullable
+  private Bundle mLaunchOptions;
+
+  @Nullable
+  private DoubleTapReloadRecognizer mDoubleTapReloadRecognizer;
+
+  private ReactNativeHost mReactNativeHost;
+
+
+  public ReactDelegate(Activity activity, ReactNativeHost reactNativeHost, @Nullable String appKey, @Nullable Bundle launchOptions) {
+    mActivity = activity;
+    mMainComponentName = appKey;
+    mLaunchOptions = launchOptions;
+    mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
+    mReactNativeHost = reactNativeHost;
+  }
+
+  public void onHostResume() {
+    if (getReactNativeHost().hasInstance()) {
+      if (mActivity instanceof DefaultHardwareBackBtnHandler) {
+        getReactNativeHost().getReactInstanceManager().onHostResume(mActivity, (DefaultHardwareBackBtnHandler) mActivity);
+      } else {
+        throw new ClassCastException("Host Activity does not implement DefaultHardwareBackBtnHandler");
+      }
+    }
+  }
+
+  public void onHostPause() {
+    if (getReactNativeHost().hasInstance()) {
+      getReactNativeHost().getReactInstanceManager().onHostPause(mActivity);
+    }
+  }
+
+  public void onHostDestroy() {
+    if (mReactRootView != null) {
+      mReactRootView.unmountReactApplication();
+      mReactRootView = null;
+    }
+    if (getReactNativeHost().hasInstance()) {
+      getReactNativeHost().getReactInstanceManager().onHostDestroy(mActivity);
+    }
+  }
+
+  public boolean onBackPressed() {
+    if (getReactNativeHost().hasInstance()) {
+      getReactNativeHost().getReactInstanceManager().onBackPressed();
+      return true;
+    }
+    return false;
+  }
+
+  public void onActivityResult(int requestCode, int resultCode, Intent data, boolean shouldForwardToReactInstance) {
+    if (getReactNativeHost().hasInstance() && shouldForwardToReactInstance) {
+      getReactNativeHost().getReactInstanceManager().onActivityResult(mActivity, requestCode, resultCode, data);
+    }
+  }
+
+  public void loadApp() {
+    loadApp(mMainComponentName);
+  }
+
+  public void loadApp(String appKey) {
+    if (mReactRootView != null) {
+      throw new IllegalStateException("Cannot loadApp while app is already running.");
+    }
+    mReactRootView = createRootView();
+    mReactRootView.startReactApplication(
+      getReactNativeHost().getReactInstanceManager(),
+      appKey,
+      mLaunchOptions);
+
+  }
+
+  public ReactRootView getReactRootView() {
+    return mReactRootView;
+  }
+
+
+  protected ReactRootView createRootView() {
+    return new ReactRootView(mActivity);
+  }
+
+  /**
+   * Handles delegating the {@link Activity#onKeyUp(int, KeyEvent)} method to determine whether
+   * the application should show the developer menu or should reload the React Application.
+   *
+   * @return true if we consume the event and either shoed the develop menu or reloaded the application.
+   */
+  public boolean shouldShowDevMenuOrReload(int keyCode, KeyEvent event) {
+    if (getReactNativeHost().hasInstance() && getReactNativeHost().getUseDeveloperSupport()) {
+      if (keyCode == KeyEvent.KEYCODE_MENU) {
+        getReactNativeHost().getReactInstanceManager().showDevOptionsDialog();
+        return true;
+      }
+      boolean didDoubleTapR = Assertions.assertNotNull(mDoubleTapReloadRecognizer).didDoubleTapR(keyCode, mActivity.getCurrentFocus());
+      if (didDoubleTapR) {
+        getReactNativeHost().getReactInstanceManager().getDevSupportManager().handleReloadJS();
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Get the {@link ReactNativeHost} used by this app.
+   */
+  private ReactNativeHost getReactNativeHost() {
+    return mReactNativeHost;
+  }
+
+  public ReactInstanceManager getReactInstanceManager() {
+    return getReactNativeHost().getReactInstanceManager();
+  }
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactFragment.java b/ReactAndroid/src/main/java/com/facebook/react/ReactFragment.java
new file mode 100644
index 00000000000000..c4341d956084ef
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/ReactFragment.java
@@ -0,0 +1,209 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.react;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.facebook.react.modules.core.PermissionAwareActivity;
+import com.facebook.react.modules.core.PermissionListener;
+
+import javax.annotation.Nullable;
+
+import androidx.fragment.app.Fragment;
+
+/**
+ * Fragment for creating a React View. This allows the developer to "embed" a React Application
+ * inside native components such as a Drawer, ViewPager, etc.
+ */
+public class ReactFragment extends Fragment implements PermissionAwareActivity {
+
+  private static final String ARG_COMPONENT_NAME = "arg_component_name";
+  private static final String ARG_LAUNCH_OPTIONS = "arg_launch_options";
+
+  private ReactDelegate mReactDelegate;
+
+  @Nullable
+  private PermissionListener mPermissionListener;
+
+
+  public ReactFragment() {
+    // Required empty public constructor
+  }
+
+  /**
+   * @param componentName The name of the react native component
+   * @return A new instance of fragment ReactFragment.
+   */
+  private static ReactFragment newInstance(String componentName, Bundle launchOptions) {
+    ReactFragment fragment = new ReactFragment();
+    Bundle args = new Bundle();
+    args.putString(ARG_COMPONENT_NAME, componentName);
+    args.putBundle(ARG_LAUNCH_OPTIONS, launchOptions);
+    fragment.setArguments(args);
+    return fragment;
+  }
+
+  // region Lifecycle
+  @Override
+  public void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    String mainComponentName = null;
+    Bundle launchOptions = null;
+    if (getArguments() != null) {
+      mainComponentName = getArguments().getString(ARG_COMPONENT_NAME);
+      launchOptions = getArguments().getBundle(ARG_LAUNCH_OPTIONS);
+    }
+    if (mainComponentName == null) {
+      throw new IllegalStateException("Cannot loadApp if component name is null");
+    }
+    mReactDelegate = new ReactDelegate(getActivity(), getReactNativeHost(), mainComponentName, launchOptions);
+  }
+
+  /**
+   * Get the {@link ReactNativeHost} used by this app. By default, assumes
+   * {@link Activity#getApplication()} is an instance of {@link ReactApplication} and calls
+   * {@link ReactApplication#getReactNativeHost()}. Override this method if your application class
+   * does not implement {@code ReactApplication} or you simply have a different mechanism for
+   * storing a {@code ReactNativeHost}, e.g. as a static field somewhere.
+   */
+  protected ReactNativeHost getReactNativeHost() {
+    return ((ReactApplication) getActivity().getApplication()).getReactNativeHost();
+  }
+
+  @Override
+  public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                           Bundle savedInstanceState) {
+    mReactDelegate.loadApp();
+    return mReactDelegate.getReactRootView();
+  }
+
+  @Override
+  public void onResume() {
+    super.onResume();
+    mReactDelegate.onHostResume();
+  }
+
+  @Override
+  public void onPause() {
+    super.onPause();
+    mReactDelegate.onHostPause();
+  }
+
+  @Override
+  public void onDestroy() {
+    super.onDestroy();
+    mReactDelegate.onHostDestroy();
+  }
+  // endregion
+
+  @Override
+  public void onActivityResult(int requestCode, int resultCode, Intent data) {
+    super.onActivityResult(requestCode, resultCode, data);
+    mReactDelegate.onActivityResult(requestCode, resultCode, data, false);
+  }
+
+  /**
+   * Helper to forward hardware back presses to our React Native Host
+   *
+   * This must be called via a forward from your host Activity
+   *
+   */
+  public boolean onBackPressed() {
+    return mReactDelegate.onBackPressed();
+  }
+
+  /**
+   * Helper to forward onKeyUp commands from our host Activity.
+   * This allows ReactFragment to handle double tap reloads and dev menus
+   *
+   * This must be called via a forward from your host Activity
+   *
+   * @param keyCode keyCode
+   * @param event   event
+   * @return true if we handled onKeyUp
+   */
+  public boolean onKeyUp(int keyCode, KeyEvent event) {
+    return mReactDelegate.shouldShowDevMenuOrReload(keyCode, event);
+  }
+
+  @Override
+  public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+    if (mPermissionListener != null &&
+      mPermissionListener.onRequestPermissionsResult(requestCode, permissions, grantResults)) {
+      mPermissionListener = null;
+    }
+  }
+
+  @Override
+  public int checkPermission(String permission, int pid, int uid) {
+    return getActivity().checkPermission(permission, pid, uid);
+  }
+
+  @TargetApi(Build.VERSION_CODES.M)
+  @Override
+  public int checkSelfPermission(String permission) {
+    return getActivity().checkSelfPermission(permission);
+  }
+
+  @TargetApi(Build.VERSION_CODES.M)
+  @Override
+  public void requestPermissions(String[] permissions, int requestCode, PermissionListener listener) {
+    mPermissionListener = listener;
+    requestPermissions(permissions, requestCode);
+  }
+
+  /**
+   * Builder class to help instantiate a ReactFragment
+   */
+  public static class Builder {
+
+    String mComponentName;
+    Bundle mLaunchOptions;
+
+    public Builder() {
+      mComponentName = null;
+      mLaunchOptions = null;
+    }
+
+    /**
+     * Set the Component name for our React Native instance.
+     *
+     * @param componentName The name of the component
+     * @return Builder
+     */
+    public Builder setComponentName(String componentName) {
+      mComponentName = componentName;
+      return this;
+    }
+
+    /**
+     * Set the Launch Options for our React Native instance.
+     *
+     * @param launchOptions launchOptions
+     * @return Builder
+     */
+    public Builder setLaunchOptions(Bundle launchOptions) {
+      mLaunchOptions = launchOptions;
+      return this;
+    }
+
+    public ReactFragment build() {
+      return ReactFragment.newInstance(mComponentName, mLaunchOptions);
+    }
+
+  }
+}