Skip to content

Commit 6b653c8

Browse files
authored
feat(android): populate home screen with AppRegistry entries (#1127)
1 parent d0b8f6d commit 6b653c8

File tree

11 files changed

+222
-40
lines changed

11 files changed

+222
-40
lines changed

android/app/build.gradle

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,15 @@ android {
158158
abiFilters(*project.ext.react.architectures)
159159
}
160160
}
161+
} else {
162+
externalNativeBuild {
163+
cmake {
164+
arguments "-DANDROID_STL=c++_shared",
165+
"-DREACT_COMMON_DIR=${reactNativePath}/ReactCommon",
166+
"-DREACT_JNILIBS_DIR=${buildDir}/outputs/jniLibs"
167+
cppFlags "-std=c++17"
168+
}
169+
}
161170
}
162171
}
163172

@@ -222,6 +231,32 @@ android {
222231
}
223232
}
224233
}
234+
} else {
235+
externalNativeBuild {
236+
cmake {
237+
path "${projectDir}/src/main/jni/CMakeLists.txt"
238+
}
239+
}
240+
241+
def version = getPackageVersion("react-native", rootDir)
242+
def allAar = file("${reactNativePath}/android/com/facebook/react/react-native/${version}/react-native-${version}.aar")
243+
244+
def prepareDebugJSI = tasks.register("prepareDebugJSI", Copy) {
245+
def debugAar = file("${reactNativePath}/android/com/facebook/react/react-native/${version}/react-native-${version}-debug.aar")
246+
from(zipTree(debugAar.exists() ? debugAar : allAar).matching({ it.include "**/libjsi.so" }))
247+
into("${buildDir}/outputs/jniLibs/debug")
248+
}
249+
250+
def prepareReleaseJSI = tasks.register("prepareReleaseJSI", Copy) {
251+
def releaseAar = file("${reactNativePath}/android/com/facebook/react/react-native/${version}/react-native-${version}-release.aar")
252+
from(zipTree(releaseAar.exists() ? releaseAar : allAar).matching({ it.include "**/libjsi.so" }))
253+
into("${buildDir}/outputs/jniLibs/release")
254+
}
255+
256+
afterEvaluate {
257+
preDebugBuild.dependsOn(prepareDebugJSI)
258+
preReleaseBuild.dependsOn(prepareReleaseJSI)
259+
}
225260
}
226261

227262
lintOptions {

android/app/src/camera/java/com/microsoft/reacttestapp/camera/MainActivityExtensions.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import androidx.core.content.ContextCompat
99
import com.google.android.material.snackbar.Snackbar
1010
import com.microsoft.reacttestapp.MainActivity
1111
import com.microsoft.reacttestapp.R
12-
import com.microsoft.reacttestapp.testApp
1312

1413
fun MainActivity.canUseCamera(): Boolean {
1514
return ContextCompat.checkSelfPermission(
@@ -27,7 +26,7 @@ fun MainActivity.scanForQrCode() {
2726
.setTitle(R.string.is_this_the_right_url)
2827
.setMessage(it)
2928
.setPositiveButton(R.string.yes) { _: DialogInterface, _: Int ->
30-
testApp.reloadJSFromServer(this, it)
29+
reloadJSFromServer(it)
3130
}
3231
.setNegativeButton(R.string.no) { _: DialogInterface, _: Int ->
3332
// Nothing to do

android/app/src/main/java/com/microsoft/reacttestapp/MainActivity.kt

Lines changed: 58 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import androidx.recyclerview.widget.DividerItemDecoration
1111
import androidx.recyclerview.widget.LinearLayoutManager
1212
import androidx.recyclerview.widget.RecyclerView
1313
import com.facebook.react.ReactActivity
14+
import com.facebook.react.bridge.ReactApplicationContext
1415
import com.facebook.react.bridge.ReactContext
1516
import com.facebook.react.modules.systeminfo.ReactNativeVersion
1617
import com.facebook.react.packagerconnection.PackagerConnectionSettings
@@ -22,6 +23,7 @@ import com.microsoft.reacttestapp.component.ComponentBottomSheetDialogFragment
2223
import com.microsoft.reacttestapp.component.ComponentListAdapter
2324
import com.microsoft.reacttestapp.component.ComponentViewModel
2425
import com.microsoft.reacttestapp.manifest.Component
26+
import com.microsoft.reacttestapp.react.AppRegistry
2527
import com.microsoft.reacttestapp.react.BundleSource
2628

2729
class MainActivity : ReactActivity() {
@@ -34,7 +36,8 @@ class MainActivity : ReactActivity() {
3436
HandlerCompat.createAsync(Looper.getMainLooper())
3537
}
3638

37-
private var didInitialNavigation = false
39+
private lateinit var componentListAdapter: ComponentListAdapter
40+
private var isTopResumedActivity = false
3841

3942
private val newComponentViewModel = { component: Component ->
4043
ComponentViewModel(
@@ -49,21 +52,7 @@ class MainActivity : ReactActivity() {
4952
Session(applicationContext)
5053
}
5154

52-
private val startComponent: (ComponentViewModel) -> Unit = { component ->
53-
didInitialNavigation = true
54-
when (component.presentationStyle) {
55-
"modal" -> {
56-
ComponentBottomSheetDialogFragment
57-
.newInstance(component)
58-
.show(supportFragmentManager, ComponentBottomSheetDialogFragment.TAG)
59-
}
60-
else -> {
61-
findActivityClass(component.name)?.let {
62-
startActivity(Intent(this, it))
63-
} ?: startActivity(ComponentActivity.newIntent(this, component))
64-
}
65-
}
66-
}
55+
private var useAppRegistry: Boolean = false
6756

6857
private fun findActivityClass(name: String): Class<*>? {
6958
return try {
@@ -87,16 +76,29 @@ class MainActivity : ReactActivity() {
8776
BuildConfig.ReactTestApp_singleApp === null -> {
8877
setContentView(R.layout.activity_main)
8978

90-
didInitialNavigation =
91-
savedInstanceState?.getBoolean("didInitialNavigation", false) == true
92-
93-
if (components.isNotEmpty()) {
79+
useAppRegistry = components.isEmpty()
80+
if (useAppRegistry) {
81+
testApp.reactNativeHost.addReactInstanceEventListener {
82+
it.runOnJSQueueThread {
83+
val appKeys = AppRegistry.getAppKeys(it as ReactApplicationContext)
84+
val viewModels = appKeys.map { appKey ->
85+
ComponentViewModel(appKey, appKey, null, null)
86+
}
87+
mainThreadHandler.post {
88+
componentListAdapter.setComponents(viewModels)
89+
if (isTopResumedActivity && viewModels.count() == 1) {
90+
startComponent(viewModels[0])
91+
}
92+
}
93+
}
94+
}
95+
} else {
9496
val index =
9597
if (components.count() == 1) 0 else session.lastOpenedComponent(checksum)
9698
index?.let {
9799
val component = newComponentViewModel(components[it])
98100
val startInitialComponent = { _: ReactContext ->
99-
if (!didInitialNavigation) {
101+
if (isTopResumedActivity) {
100102
startComponent(component)
101103
}
102104
}
@@ -125,6 +127,11 @@ class MainActivity : ReactActivity() {
125127
}
126128
}
127129

130+
override fun onTopResumedActivityChanged(isTopResumedActivity: Boolean) {
131+
super.onTopResumedActivityChanged(isTopResumedActivity)
132+
this.isTopResumedActivity = isTopResumedActivity
133+
}
134+
128135
override fun onRequestPermissionsResult(
129136
requestCode: Int,
130137
permissions: Array<out String>,
@@ -139,26 +146,30 @@ class MainActivity : ReactActivity() {
139146
}
140147
}
141148

142-
override fun onSaveInstanceState(outState: Bundle) {
143-
outState.putBoolean("didInitialNavigation", didInitialNavigation)
144-
super.onSaveInstanceState(outState)
149+
internal fun reloadJSFromServer(bundleURL: String) {
150+
componentListAdapter.clear()
151+
testApp.reloadJSFromServer(this, bundleURL)
145152
}
146153

147154
private fun reload(bundleSource: BundleSource) {
155+
if (useAppRegistry) {
156+
componentListAdapter.clear()
157+
}
148158
testApp.reactNativeHost.reload(this, bundleSource)
149159
}
150160

151161
private fun setupRecyclerView(manifestComponents: List<Component>, manifestChecksum: String) {
152-
val components = manifestComponents.map(newComponentViewModel)
162+
componentListAdapter = ComponentListAdapter(
163+
LayoutInflater.from(applicationContext),
164+
manifestComponents.map(newComponentViewModel)
165+
) { component, index ->
166+
startComponent(component)
167+
session.storeComponent(index, manifestChecksum)
168+
}
169+
153170
findViewById<RecyclerView>(R.id.recyclerview).apply {
154171
layoutManager = LinearLayoutManager(context)
155-
adapter = ComponentListAdapter(
156-
LayoutInflater.from(context),
157-
components
158-
) { component, index ->
159-
startComponent(component)
160-
session.storeComponent(index, manifestChecksum)
161-
}
172+
adapter = componentListAdapter
162173

163174
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
164175
}
@@ -214,6 +225,21 @@ class MainActivity : ReactActivity() {
214225
}
215226
}
216227

228+
private fun startComponent(component: ComponentViewModel) {
229+
when (component.presentationStyle) {
230+
"modal" -> {
231+
ComponentBottomSheetDialogFragment
232+
.newInstance(component)
233+
.show(supportFragmentManager, ComponentBottomSheetDialogFragment.TAG)
234+
}
235+
else -> {
236+
findActivityClass(component.name)?.let {
237+
startActivity(Intent(this, it))
238+
} ?: startActivity(ComponentActivity.newIntent(this, component))
239+
}
240+
}
241+
}
242+
217243
private fun updateMenuItemState(toolbar: MaterialToolbar, bundleSource: BundleSource) {
218244
toolbar.menu.apply {
219245
findItem(R.id.load_embedded_js_bundle).isEnabled =

android/app/src/main/java/com/microsoft/reacttestapp/component/ComponentListAdapter.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,33 @@
11
package com.microsoft.reacttestapp.component
22

3+
import android.annotation.SuppressLint
34
import android.view.LayoutInflater
45
import android.view.ViewGroup
56
import android.widget.TextView
7+
import androidx.annotation.UiThread
68
import androidx.recyclerview.widget.RecyclerView
79
import com.microsoft.reacttestapp.R
810

911
class ComponentListAdapter(
1012
private val layoutInflater: LayoutInflater,
11-
private val components: List<ComponentViewModel>,
13+
private var components: List<ComponentViewModel>,
1214
private val listener: (ComponentViewModel, Int) -> Unit
1315
) : RecyclerView.Adapter<ComponentListAdapter.ComponentViewHolder>() {
1416

17+
@UiThread
18+
fun clear() {
19+
val itemCount = components.size
20+
components = listOf()
21+
notifyItemRangeRemoved(0, itemCount)
22+
}
23+
24+
@SuppressLint("NotifyDataSetChanged")
25+
@UiThread
26+
fun setComponents(components: List<ComponentViewModel>) {
27+
this.components = components
28+
notifyDataSetChanged()
29+
}
30+
1531
override fun getItemCount() = components.size
1632

1733
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ComponentViewHolder {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.microsoft.reacttestapp.react
2+
3+
import com.facebook.react.bridge.ReactApplicationContext
4+
import com.facebook.soloader.SoLoader
5+
6+
/**
7+
* The corresponding C++ implementation is in `android/app/src/main/jni/AppRegistry.cpp`
8+
*/
9+
class AppRegistry {
10+
companion object {
11+
init {
12+
SoLoader.loadLibrary("reacttestapp_appmodules")
13+
}
14+
15+
fun getAppKeys(context: ReactApplicationContext): Array<String> {
16+
val appKeys = AppRegistry().getAppKeys(context.javaScriptContextHolder.get())
17+
?: return arrayOf()
18+
return appKeys.filterIsInstance<String>().toTypedArray()
19+
}
20+
}
21+
22+
private external fun getAppKeys(jsiPtr: Long): Array<Any>?
23+
}

android/app/src/main/java/com/microsoft/reacttestapp/react/TestAppReactNativeHost.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import com.microsoft.reacttestapp.R
2727
import com.microsoft.reacttestapp.compat.ReactInstanceEventListener
2828
import com.microsoft.reacttestapp.compat.ReactNativeHostCompat
2929
import com.microsoft.reacttestapp.fabric.FabricJSIModulePackage
30+
import java.util.Collections.synchronizedList
3031
import java.util.concurrent.CountDownLatch
3132

3233
sealed class BundleSource {
@@ -63,6 +64,9 @@ class TestAppReactNativeHost(
6364

6465
var onBundleSourceChanged: ((newSource: BundleSource) -> Unit)? = null
6566

67+
private val reactInstanceEventListeners =
68+
synchronizedList<ReactInstanceEventListener>(arrayListOf())
69+
6670
fun init(beforeReactNativeInit: () -> Unit, afterReactNativeInit: () -> Unit) {
6771
if (BuildConfig.DEBUG && hasInstance()) {
6872
error("init() can only be called once on startup")
@@ -106,6 +110,7 @@ class TestAppReactNativeHost(
106110
}
107111

108112
fun addReactInstanceEventListener(listener: (ReactContext) -> Unit) {
113+
reactInstanceEventListeners.add(listener)
109114
reactInstanceManager.addReactInstanceEventListener(listener)
110115
}
111116

@@ -162,7 +167,16 @@ class TestAppReactNativeHost(
162167
.setJSIModulesPackage(jsiModulePackage)
163168
.build()
164169
ReactMarker.logMarker(ReactMarkerConstants.BUILD_REACT_INSTANCE_MANAGER_END)
170+
165171
addCustomDevOptions(reactInstanceManager.devSupportManager)
172+
173+
synchronized(reactInstanceEventListeners) {
174+
val i = reactInstanceEventListeners.iterator()
175+
while (i.hasNext()) {
176+
reactInstanceManager.addReactInstanceEventListener(i.next())
177+
}
178+
}
179+
166180
return reactInstanceManager
167181
}
168182

android/app/src/main/jni/Android.mk

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ include $(PROJECT_BUILD_DIR)/generated/rncli/src/main/jni/Android-rncli.mk
1212
include $(CLEAR_VARS)
1313

1414
LOCAL_PATH := $(THIS_DIR)
15+
REACTTESTAPP_PATH := $(LOCAL_PATH)/../../../../..
1516

1617
# You can customize the name of your application .so file here.
1718
LOCAL_MODULE := reacttestapp_appmodules
1819

19-
LOCAL_C_INCLUDES := $(LOCAL_PATH)
20-
LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp)
20+
LOCAL_C_INCLUDES := $(LOCAL_PATH) $(REACTTESTAPP_PATH)
21+
LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp) $(REACTTESTAPP_PATH)/common/AppRegistry.cpp
2122
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)
2223

2324
# If you wish to add a custom TurboModule or Fabric component in your app you
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#include "AppRegistry.h"
2+
3+
#include "common/AppRegistry.h"
4+
5+
extern "C" {
6+
7+
JNIEXPORT jobjectArray JNICALL Java_com_microsoft_reacttestapp_react_AppRegistry_getAppKeys(
8+
JNIEnv *env, jclass clazz, jlong jsiPtr)
9+
{
10+
auto runtime = reinterpret_cast<facebook::jsi::Runtime *>(jsiPtr);
11+
auto appKeys = ReactTestApp::GetAppKeys(*runtime);
12+
auto numKeys = static_cast<int>(appKeys.size());
13+
auto result = env->NewObjectArray(numKeys, env->FindClass("java/lang/String"), nullptr);
14+
for (int i = 0; i < numKeys; ++i) {
15+
env->SetObjectArrayElement(result, i, env->NewStringUTF(appKeys[i].c_str()));
16+
}
17+
return result;
18+
}
19+
20+
} // extern "C"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#ifndef ANDROID_JNI_APPREGISTRY_
2+
#define ANDROID_JNI_APPREGISTRY_
3+
4+
#include <jni.h>
5+
6+
extern "C" {
7+
8+
JNIEXPORT jobjectArray JNICALL Java_com_microsoft_reacttestapp_react_AppRegistry_getAppKeys(
9+
JNIEnv *env, jclass clazz, jlong jsiPtr);
10+
11+
} // extern "C"
12+
13+
#endif // ANDROID_JNI_APPREGISTRY_

0 commit comments

Comments
 (0)