Skip to content
This repository was archived by the owner on May 20, 2025. It is now read-only.

Commit f6ea4ec

Browse files
committed
[Android] Support React instances with no Activity
This change aims to support applications which run React Native's catalyst instance in the background, sometimes without an activity. We're using this change today in an RN 0.27 application, which overrides `ReactActivity#createReactInstanceManager` to afford us control of the instance's lifecycle, so that we may run and launch without an `Activity` (i.e., in response to push notifications and other triggers). If we agree with this approach, I'll update this PR with documentation/etc.
1 parent 2a182ab commit f6ea4ec

File tree

5 files changed

+141
-55
lines changed

5 files changed

+141
-55
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ gen/
132132
.gradle/
133133
build/
134134
*/build/
135+
android/app/gradle*
135136

136137
# Local configuration file (sdk path, etc)
137138
local.properties
@@ -149,4 +150,4 @@ proguard/
149150
captures/
150151

151152
# Remove after this framework is published on NPM
152-
code-push-plugin-testing-framework/node_modules
153+
code-push-plugin-testing-framework/node_modules

README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,57 @@ public class MainActivity extends ReactActivity {
343343
}
344344
```
345345

346+
#### Background React Instances ####
347+
348+
**This section is only necessary if you're *explicitly* launching a React Native instance without an `Activity` (for example, from within a native push notification receiver). For these situations, CodePush must be told how to find your React Native instance.**
349+
350+
In order to update/restart your React Native instance, CodePush must be configured with a `ReactInstanceHolder` before attempting to restart an instance in the background. This is usually done in your `Application` implementation.
351+
352+
**For React Native >= v0.29**
353+
354+
Update the `MainApplication.java` file to use CodePush via the following changes:
355+
356+
```java
357+
...
358+
// 1. Declare your ReactNativeHost to extend ReactInstanceHolder. ReactInstanceHolder is a subset of ReactNativeHost, so no additional implementation is needed.
359+
import com.microsoft.codepush.react.ReactInstanceHolder;
360+
361+
public class MyReactNativeHost extends ReactNativeHost implements ReactInstanceHolder {
362+
// ... usual overrides
363+
}
364+
365+
// 2. Provide your ReactNativeHost to CodePush.
366+
367+
public class MainApplication extends Application implements ReactApplication {
368+
369+
private final MyReactNativeHost mReactNativeHost = new MyReactNativeHost(this);
370+
371+
@Override
372+
public void onCreate() {
373+
CodePush.setReactInstanceHolder(mReactNativeHost);
374+
super.onCreate();
375+
}
376+
}
377+
```
378+
379+
**For React Native v0.19 - v0.28**
380+
381+
Before v0.29, React Native did not provide a `ReactNativeHost` abstraction. If you're launching a background instance, you'll likely have built your own, which should now implement `ReactInstanceHolder`. Once that's done...
382+
383+
```java
384+
// 1. Provide your ReactInstanceHolder to CodePush.
385+
386+
public class MainApplication extends Application {
387+
388+
@Override
389+
public void onCreate() {
390+
// ... initialize your instance holder
391+
CodePush.setReactInstanceHolder(myInstanceHolder);
392+
super.onCreate();
393+
}
394+
}
395+
```
396+
346397
In order to effectively make use of the `Staging` and `Production` deployments that were created along with your CodePush app, refer to the [multi-deployment testing](#multi-deployment-testing) docs below before actually moving your app's usage of CodePush into production.
347398

348399
## Windows Setup

android/app/src/main/java/com/microsoft/codepush/react/CodePush.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.microsoft.codepush.react;
22

3+
import com.facebook.react.ReactInstanceManager;
34
import com.facebook.react.ReactPackage;
45
import com.facebook.react.bridge.JavaScriptModule;
56
import com.facebook.react.bridge.NativeModule;
@@ -44,6 +45,7 @@ public class CodePush implements ReactPackage {
4445
private Context mContext;
4546
private final boolean mIsDebugMode;
4647

48+
private static ReactInstanceHolder mReactInstanceHolder;
4749
private static CodePush mCurrentInstance;
4850

4951
public CodePush(String deploymentKey, Context context) {
@@ -277,6 +279,17 @@ public void clearUpdates() {
277279
mSettingsManager.removeFailedUpdates();
278280
}
279281

282+
public static void setReactInstanceHolder(ReactInstanceHolder reactInstanceHolder) {
283+
mReactInstanceHolder = reactInstanceHolder;
284+
}
285+
286+
static ReactInstanceManager getReactInstanceManager() {
287+
if (mReactInstanceHolder == null) {
288+
return null;
289+
}
290+
return mReactInstanceHolder.getReactInstanceManager();
291+
}
292+
280293
@Override
281294
public List<NativeModule> createNativeModules(ReactApplicationContext reactApplicationContext) {
282295
CodePushNativeModule codePushModule = new CodePushNativeModule(reactApplicationContext, this, mUpdateManager, mTelemetryManager, mSettingsManager);
@@ -297,4 +310,4 @@ public List<Class<? extends JavaScriptModule>> createJSModules() {
297310
public List<ViewManager> createViewManagers(ReactApplicationContext reactApplicationContext) {
298311
return new ArrayList<>();
299312
}
300-
}
313+
}

android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java

Lines changed: 57 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package com.microsoft.codepush.react;
22

33
import android.app.Activity;
4-
import android.content.Context;
54
import android.os.AsyncTask;
5+
import android.os.Handler;
6+
import android.os.Looper;
67
import android.provider.Settings;
78
import android.view.Choreographer;
89

@@ -35,7 +36,7 @@ public class CodePushNativeModule extends ReactContextBaseJavaModule {
3536
private String mClientUniqueId = null;
3637
private LifecycleEventListener mLifecycleEventListener = null;
3738
private int mMinimumBackgroundDuration = 0;
38-
39+
3940
private CodePush mCodePush;
4041
private SettingsManager mSettingsManager;
4142
private CodePushTelemetryManager mTelemetryManager;
@@ -77,16 +78,13 @@ public String getName() {
7778
return "CodePush";
7879
}
7980

80-
private boolean isReactApplication(Context context) {
81-
Class<?> reactApplicationClass = tryGetClass(REACT_APPLICATION_CLASS_NAME);
82-
if (reactApplicationClass != null && reactApplicationClass.isInstance(context)) {
83-
return true;
81+
private void loadBundleLegacy() {
82+
final Activity currentActivity = getCurrentActivity();
83+
if (currentActivity == null) {
84+
// The currentActivity can be null if it is backgrounded / destroyed, so we simply
85+
// no-op to prevent any null pointer exceptions.
86+
return;
8487
}
85-
86-
return false;
87-
}
88-
89-
private void loadBundleLegacy(final Activity currentActivity) {
9088
mCodePush.invalidateCurrentInstance();
9189

9290
currentActivity.runOnUiThread(new Runnable() {
@@ -99,41 +97,14 @@ public void run() {
9997

10098
private void loadBundle() {
10199
mCodePush.clearDebugCacheIfNeeded();
102-
final Activity currentActivity = getCurrentActivity();
103-
104-
if (currentActivity == null) {
105-
// The currentActivity can be null if it is backgrounded / destroyed, so we simply
106-
// no-op to prevent any null pointer exceptions.
107-
return;
108-
}
109-
110100
try {
111-
ReactInstanceManager instanceManager;
112101
// #1) Get the ReactInstanceManager instance, which is what includes the
113102
// logic to reload the current React context.
114-
try {
115-
// In RN >=0.29, the "mReactInstanceManager" field yields a null value, so we try
116-
// to get the instance manager via the ReactNativeHost, which only exists in 0.29.
117-
Method getApplicationMethod = ReactActivity.class.getMethod("getApplication");
118-
Object reactApplication = getApplicationMethod.invoke(currentActivity);
119-
Class<?> reactApplicationClass = tryGetClass(REACT_APPLICATION_CLASS_NAME);
120-
Method getReactNativeHostMethod = reactApplicationClass.getMethod("getReactNativeHost");
121-
Object reactNativeHost = getReactNativeHostMethod.invoke(reactApplication);
122-
Class<?> reactNativeHostClass = tryGetClass(REACT_NATIVE_HOST_CLASS_NAME);
123-
Method getReactInstanceManagerMethod = reactNativeHostClass.getMethod("getReactInstanceManager");
124-
instanceManager = (ReactInstanceManager)getReactInstanceManagerMethod.invoke(reactNativeHost);
125-
} catch (Exception e) {
126-
// The React Native version might be older than 0.29, or the activity does not
127-
// extend ReactActivity, so we try to get the instance manager via the
128-
// "mReactInstanceManager" field.
129-
Class instanceManagerHolderClass = currentActivity instanceof ReactActivity
130-
? ReactActivity.class
131-
: currentActivity.getClass();
132-
Field instanceManagerField = instanceManagerHolderClass.getDeclaredField("mReactInstanceManager");
133-
instanceManagerField.setAccessible(true);
134-
instanceManager = (ReactInstanceManager)instanceManagerField.get(currentActivity);
103+
final ReactInstanceManager instanceManager = resolveInstanceManager();
104+
if (instanceManager == null) {
105+
return;
135106
}
136-
107+
137108
String latestJSBundleFile = mCodePush.getJSBundleFileInternal(mCodePush.getAssetsBundleFileName());
138109

139110
// #2) Update the locally stored JS bundle file path
@@ -143,27 +114,60 @@ private void loadBundle() {
143114

144115
// #3) Get the context creation method and fire it on the UI thread (which RN enforces)
145116
final Method recreateMethod = instanceManager.getClass().getMethod("recreateReactContextInBackground");
146-
147-
final ReactInstanceManager finalizedInstanceManager = instanceManager;
148-
currentActivity.runOnUiThread(new Runnable() {
117+
new Handler(Looper.getMainLooper()).post(new Runnable() {
149118
@Override
150119
public void run() {
151120
try {
152-
recreateMethod.invoke(finalizedInstanceManager);
121+
recreateMethod.invoke(instanceManager);
153122
mCodePush.initializeUpdateAfterRestart();
154-
}
155-
catch (Exception e) {
123+
} catch (Exception e) {
156124
// The recreation method threw an unknown exception
157-
// so just simply fallback to restarting the Activity
158-
loadBundleLegacy(currentActivity);
125+
// so just simply fallback to restarting the Activity (if it exists)
126+
loadBundleLegacy();
159127
}
160128
}
161129
});
130+
162131
} catch (Exception e) {
163132
// Our reflection logic failed somewhere
164-
// so fall back to restarting the Activity
165-
loadBundleLegacy(currentActivity);
133+
// so fall back to restarting the Activity (if it exists)
134+
loadBundleLegacy();
135+
}
136+
}
137+
138+
private ReactInstanceManager resolveInstanceManager() throws NoSuchFieldException, IllegalAccessException {
139+
ReactInstanceManager instanceManager = CodePush.getReactInstanceManager();
140+
if (instanceManager != null) {
141+
return instanceManager;
142+
}
143+
144+
final Activity currentActivity = getCurrentActivity();
145+
if (currentActivity == null) {
146+
return null;
147+
}
148+
try {
149+
// In RN >=0.29, the "mReactInstanceManager" field yields a null value, so we try
150+
// to get the instance manager via the ReactNativeHost, which only exists in 0.29.
151+
Method getApplicationMethod = ReactActivity.class.getMethod("getApplication");
152+
Object reactApplication = getApplicationMethod.invoke(currentActivity);
153+
Class<?> reactApplicationClass = tryGetClass(REACT_APPLICATION_CLASS_NAME);
154+
Method getReactNativeHostMethod = reactApplicationClass.getMethod("getReactNativeHost");
155+
Object reactNativeHost = getReactNativeHostMethod.invoke(reactApplication);
156+
Class<?> reactNativeHostClass = tryGetClass(REACT_NATIVE_HOST_CLASS_NAME);
157+
Method getReactInstanceManagerMethod = reactNativeHostClass.getMethod("getReactInstanceManager");
158+
instanceManager = (ReactInstanceManager)getReactInstanceManagerMethod.invoke(reactNativeHost);
159+
} catch (Exception e) {
160+
// The React Native version might be older than 0.29, or the activity does not
161+
// extend ReactActivity, so we try to get the instance manager via the
162+
// "mReactInstanceManager" field.
163+
Class instanceManagerHolderClass = currentActivity instanceof ReactActivity
164+
? ReactActivity.class
165+
: currentActivity.getClass();
166+
Field instanceManagerField = instanceManagerHolderClass.getDeclaredField("mReactInstanceManager");
167+
instanceManagerField.setAccessible(true);
168+
instanceManager = (ReactInstanceManager)instanceManagerField.get(currentActivity);
166169
}
170+
return instanceManager;
167171
}
168172

169173
private Class tryGetClass(String className) {
@@ -493,4 +497,4 @@ public void downloadAndReplaceCurrentBundle(String remoteBundleUrl) {
493497
}
494498
}
495499
}
496-
}
500+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.microsoft.codepush.react;
2+
3+
import com.facebook.react.ReactInstanceManager;
4+
5+
/**
6+
* Provides access to a {@link ReactInstanceManager}.
7+
*
8+
* ReactNativeHost already implements this interface, if you make use of that react-native
9+
* component (just add `implements ReactInstanceHolder`).
10+
*/
11+
public interface ReactInstanceHolder {
12+
13+
/**
14+
* Get the current {@link ReactInstanceManager} instance. May return null.
15+
*/
16+
ReactInstanceManager getReactInstanceManager();
17+
}

0 commit comments

Comments
 (0)