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); + } + + } +}