Skip to content

Commit c376c30

Browse files
authored
Merge pull request microsoft#491 from igrayson/patch-1
[Android] Support React instances with no Activity
2 parents c2d2e35 + f6ea4ec commit c376c30

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

@@ -36,7 +37,7 @@ public class CodePushNativeModule extends ReactContextBaseJavaModule {
3637
private String mClientUniqueId = null;
3738
private LifecycleEventListener mLifecycleEventListener = null;
3839
private int mMinimumBackgroundDuration = 0;
39-
40+
4041
private CodePush mCodePush;
4142
private SettingsManager mSettingsManager;
4243
private CodePushTelemetryManager mTelemetryManager;
@@ -78,16 +79,13 @@ public String getName() {
7879
return "CodePush";
7980
}
8081

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

9391
currentActivity.runOnUiThread(new Runnable() {
@@ -100,41 +98,14 @@ public void run() {
10098

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

140111
// #2) Update the locally stored JS bundle file path
@@ -155,27 +126,60 @@ private void loadBundle() {
155126

156127
// #3) Get the context creation method and fire it on the UI thread (which RN enforces)
157128
final Method recreateMethod = instanceManager.getClass().getMethod("recreateReactContextInBackground");
158-
159-
final ReactInstanceManager finalizedInstanceManager = instanceManager;
160-
currentActivity.runOnUiThread(new Runnable() {
129+
new Handler(Looper.getMainLooper()).post(new Runnable() {
161130
@Override
162131
public void run() {
163132
try {
164-
recreateMethod.invoke(finalizedInstanceManager);
133+
recreateMethod.invoke(instanceManager);
165134
mCodePush.initializeUpdateAfterRestart();
166-
}
167-
catch (Exception e) {
135+
} catch (Exception e) {
168136
// The recreation method threw an unknown exception
169-
// so just simply fallback to restarting the Activity
170-
loadBundleLegacy(currentActivity);
137+
// so just simply fallback to restarting the Activity (if it exists)
138+
loadBundleLegacy();
171139
}
172140
}
173141
});
142+
174143
} catch (Exception e) {
175144
// Our reflection logic failed somewhere
176-
// so fall back to restarting the Activity
177-
loadBundleLegacy(currentActivity);
145+
// so fall back to restarting the Activity (if it exists)
146+
loadBundleLegacy();
147+
}
148+
}
149+
150+
private ReactInstanceManager resolveInstanceManager() throws NoSuchFieldException, IllegalAccessException {
151+
ReactInstanceManager instanceManager = CodePush.getReactInstanceManager();
152+
if (instanceManager != null) {
153+
return instanceManager;
154+
}
155+
156+
final Activity currentActivity = getCurrentActivity();
157+
if (currentActivity == null) {
158+
return null;
159+
}
160+
try {
161+
// In RN >=0.29, the "mReactInstanceManager" field yields a null value, so we try
162+
// to get the instance manager via the ReactNativeHost, which only exists in 0.29.
163+
Method getApplicationMethod = ReactActivity.class.getMethod("getApplication");
164+
Object reactApplication = getApplicationMethod.invoke(currentActivity);
165+
Class<?> reactApplicationClass = tryGetClass(REACT_APPLICATION_CLASS_NAME);
166+
Method getReactNativeHostMethod = reactApplicationClass.getMethod("getReactNativeHost");
167+
Object reactNativeHost = getReactNativeHostMethod.invoke(reactApplication);
168+
Class<?> reactNativeHostClass = tryGetClass(REACT_NATIVE_HOST_CLASS_NAME);
169+
Method getReactInstanceManagerMethod = reactNativeHostClass.getMethod("getReactInstanceManager");
170+
instanceManager = (ReactInstanceManager)getReactInstanceManagerMethod.invoke(reactNativeHost);
171+
} catch (Exception e) {
172+
// The React Native version might be older than 0.29, or the activity does not
173+
// extend ReactActivity, so we try to get the instance manager via the
174+
// "mReactInstanceManager" field.
175+
Class instanceManagerHolderClass = currentActivity instanceof ReactActivity
176+
? ReactActivity.class
177+
: currentActivity.getClass();
178+
Field instanceManagerField = instanceManagerHolderClass.getDeclaredField("mReactInstanceManager");
179+
instanceManagerField.setAccessible(true);
180+
instanceManager = (ReactInstanceManager)instanceManagerField.get(currentActivity);
178181
}
182+
return instanceManager;
179183
}
180184

181185
private Class tryGetClass(String className) {
@@ -505,4 +509,4 @@ public void downloadAndReplaceCurrentBundle(String remoteBundleUrl) {
505509
}
506510
}
507511
}
508-
}
512+
}
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)