package com.microsoft.reacttestapp import android.app.Activity import android.content.DialogInterface import android.content.Intent import android.os.Bundle import android.os.Looper import android.view.LayoutInflater import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.core.os.HandlerCompat import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.facebook.react.ReactActivity import com.facebook.react.ReactInstanceEventListener import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContext import com.facebook.react.modules.systeminfo.ReactNativeVersion import com.facebook.react.packagerconnection.PackagerConnectionSettings import com.google.android.material.appbar.MaterialToolbar import com.microsoft.reacttestapp.camera.canUseCamera import com.microsoft.reacttestapp.camera.scanForQrCode import com.microsoft.reacttestapp.component.ComponentActivity import com.microsoft.reacttestapp.component.ComponentBottomSheetDialogFragment import com.microsoft.reacttestapp.component.ComponentListAdapter import com.microsoft.reacttestapp.component.ComponentViewModel import com.microsoft.reacttestapp.manifest.Component import com.microsoft.reacttestapp.manifest.ManifestProvider import com.microsoft.reacttestapp.react.AppRegistry import com.microsoft.reacttestapp.react.BundleSource class MainActivity : ReactActivity() { companion object { const val REQUEST_CODE_PERMISSIONS = 42 } val mainThreadHandler by lazy { HandlerCompat.createAsync(Looper.getMainLooper()) } private lateinit var componentListAdapter: ComponentListAdapter private var isTopResumedActivity = false private val isMissingJSBundle get() = testApp.reactNativeHost.source == BundleSource.Disk && testApp.bundleNameProvider.bundleName == null private val newComponentViewModel = { component: Component -> ComponentViewModel( component.appKey, component.displayName ?: component.appKey, component.initialProperties, component.presentationStyle ) } private val session by lazy { Session(applicationContext) } private var useAppRegistry: Boolean = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val checksum = ManifestProvider.checksum() val manifest = testApp.manifest val components = manifest.components ?: listOf() @Suppress("SENSELESS_COMPARISON") when { BuildConfig.REACTAPP_SINGLE_APP === null -> { setContentView(R.layout.activity_main) useAppRegistry = components.isEmpty() if (useAppRegistry) { testApp.reactNativeHost.addReactInstanceEventListener( object : ReactInstanceEventListener { override fun onReactContextInitialized(context: ReactContext) { (context as? ReactApplicationContext)?.runOnJSQueueThread { val appKeys = AppRegistry.getAppKeys(context) val viewModels = appKeys.map { appKey -> ComponentViewModel(appKey, appKey, null, null) } mainThreadHandler.post { componentListAdapter.setComponents(viewModels) if (isTopResumedActivity && viewModels.count() == 1) { startComponent(viewModels[0]) } } } } } ) } else { val singleComponent = components.count() == 1 val index = if (singleComponent) 0 else session.lastOpenedComponent(checksum) index?.let { val component = components[it] startComponent(newComponentViewModel(component)) } } setupToolbar(manifest.displayName) setupRecyclerView(components, checksum) } components.isNotEmpty() -> { val slug = BuildConfig.REACTAPP_SINGLE_APP val component = components.find { it.slug == slug } ?: throw IllegalArgumentException("No component with slug: $slug") val intent = ComponentActivity.newIntent(this, newComponentViewModel(component)) intent.flags = Intent.FLAG_ACTIVITY_TASK_ON_HOME or Intent.FLAG_ACTIVITY_NEW_TASK startActivity(intent) finish() } else -> throw IllegalArgumentException("At least one component must be declared") } } override fun onTopResumedActivityChanged(isTopResumedActivity: Boolean) { super.onTopResumedActivityChanged(isTopResumedActivity) this.isTopResumedActivity = isTopResumedActivity } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, grantResults: IntArray ) { if (requestCode == REQUEST_CODE_PERMISSIONS) { if (canUseCamera()) { scanForQrCode() } } else { super.onRequestPermissionsResult(requestCode, permissions, grantResults) } } internal fun reloadJSFromServer(bundleURL: String) { componentListAdapter.clear() testApp.reloadJSFromServer(this, bundleURL) } private fun findActivityClass(name: String): Class<*>? { return try { val result = Class.forName(name) val isActivity = Activity::class.java.isAssignableFrom(result) return if (isActivity) result else null } catch (e: ClassNotFoundException) { null } } private fun reload(bundleSource: BundleSource) { if (useAppRegistry) { componentListAdapter.clear() } testApp.reactNativeHost.reload(this, bundleSource) } private fun setupRecyclerView(manifestComponents: List, manifestChecksum: String) { componentListAdapter = ComponentListAdapter( LayoutInflater.from(applicationContext), manifestComponents.map(newComponentViewModel) ) { component, index -> startComponent(component) session.storeComponent(index, manifestChecksum) } findViewById(R.id.recyclerview).apply { layoutManager = LinearLayoutManager(context) adapter = componentListAdapter addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) } findViewById(R.id.runtime_info).apply { text = resources.getString( R.string.runtime_info, ReactNativeVersion.VERSION["major"] as Int, ReactNativeVersion.VERSION["minor"] as Int, ReactNativeVersion.VERSION["patch"] as Int, testApp.reactNativeHost.jsExecutorName, if (BuildConfig.REACTAPP_USE_FABRIC) "+Fabric" else "" ) } } private fun setupToolbar(displayName: String) { val toolbar = findViewById(R.id.top_app_bar) toolbar.title = displayName toolbar.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { R.id.load_embedded_js_bundle -> { reload(BundleSource.Disk) true } R.id.load_from_dev_server -> { PackagerConnectionSettings(this).debugServerHost = "" reload(BundleSource.Server) true } R.id.remember_last_component -> { val enable = !menuItem.isChecked menuItem.isChecked = enable session.shouldRememberLastComponent = enable true } R.id.scan_qr_code -> { scanForQrCode() true } R.id.show_dev_options -> { val devSupportManager = if (BuildConfig.REACTAPP_USE_BRIDGELESS) { (application as TestApp).reactHost.devSupportManager } else { reactInstanceManager.devSupportManager } devSupportManager?.showDevOptionsDialog() true } else -> false } } updateMenuItemState(toolbar, testApp.reactNativeHost.source) testApp.reactNativeHost.onBundleSourceChanged = { updateMenuItemState(toolbar, it) } } private fun startComponent(component: ComponentViewModel) { if (isMissingJSBundle) { AlertDialog .Builder(this) .setTitle(R.string.missing_js_bundle) .setMessage(R.string.missing_js_bundle_description) .setPositiveButton(R.string.ok) { _: DialogInterface, _: Int -> // Nothing to do } .show() return } when (component.presentationStyle) { "modal" -> { ComponentBottomSheetDialogFragment .newInstance(component) .show(supportFragmentManager, ComponentBottomSheetDialogFragment.TAG) } else -> { findActivityClass(component.name)?.let { startActivity(Intent(this, it)) } ?: startActivity(ComponentActivity.newIntent(this, component)) } } } private fun updateMenuItemState(toolbar: MaterialToolbar, bundleSource: BundleSource) { toolbar.menu.apply { findItem(R.id.load_embedded_js_bundle).isEnabled = testApp.bundleNameProvider.bundleName != null findItem(R.id.remember_last_component).isChecked = session.shouldRememberLastComponent findItem(R.id.show_dev_options).isEnabled = bundleSource == BundleSource.Server } } }