1 package expo.modules.devmenu 2 3 import android.os.Build 4 import android.os.Bundle 5 import android.view.KeyEvent 6 import android.view.View 7 import android.view.ViewGroup 8 import android.widget.FrameLayout 9 import androidx.coordinatorlayout.widget.CoordinatorLayout 10 import androidx.core.view.doOnLayout 11 import com.facebook.react.ReactActivity 12 import com.facebook.react.ReactActivityDelegate 13 import com.facebook.react.ReactDelegate 14 import com.facebook.react.ReactInstanceManager 15 import com.facebook.react.ReactRootView 16 import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled 17 import com.facebook.react.defaults.DefaultReactActivityDelegate 18 import com.facebook.react.devsupport.interfaces.DevSupportManager 19 import com.google.android.material.bottomsheet.BottomSheetBehavior 20 import expo.modules.devmenu.helpers.getPrivateDeclaredFieldValue 21 import expo.modules.devmenu.helpers.setPrivateDeclaredFieldValue 22 import java.util.* 23 24 /** 25 * The dev menu is launched using this activity. 26 * [DevMenuActivity] is transparent and doesn't have any in/out animations. 27 * So we can display dev menu as a modal. 28 */ 29 class DevMenuActivity : ReactActivity() { getMainComponentNamenull30 override fun getMainComponentName() = "main" 31 32 private val isEmulator 33 get() = Build.FINGERPRINT.contains("vbox") || Build.FINGERPRINT.contains("generic") 34 35 override fun createReactActivityDelegate(): ReactActivityDelegate { 36 return object : DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) { 37 // We don't want to destroy the root view, because we want to reuse it later. 38 override fun onDestroy() = Unit 39 40 override fun loadApp(appKey: String?) { 41 // On the first launch of this activity we need to call super.loadApp() to start the dev menu 42 if (!appWasLoaded) { 43 super.loadApp(appKey) 44 appWasLoaded = true 45 return 46 } 47 48 val reactDelegate: ReactDelegate = ReactActivityDelegate::class.java 49 .getPrivateDeclaredFieldValue("mReactDelegate", this) 50 51 ReactDelegate::class.java 52 .setPrivateDeclaredFieldValue("mFabricEnabled", reactDelegate, fabricEnabled) 53 ReactDelegate::class.java 54 .setPrivateDeclaredFieldValue("mReactRootView", reactDelegate, rootView) 55 56 // Removes the root view from the previous activity 57 (rootView.parent as? ViewGroup)?.removeView(rootView) 58 59 // Attaches the root view to the current activity 60 plainActivity.setContentView(reactDelegate.getReactRootView()) 61 } 62 63 override fun getReactNativeHost() = DevMenuManager.getMenuHost() 64 65 override fun getLaunchOptions() = Bundle().apply { 66 putString("uuid", UUID.randomUUID().toString()) 67 putBundle("appInfo", DevMenuManager.getAppInfo()) 68 putBundle("devSettings", DevMenuManager.getDevSettings()) 69 putBundle("menuPreferences", DevMenuManager.getMenuPreferences()) 70 putBoolean("isDevice", !isEmulator) 71 putStringArray("registeredCallbacks", DevMenuManager.registeredCallbacks.map { it.name }.toTypedArray()) 72 } 73 74 override fun createRootView(): ReactRootView { 75 if (rootViewWasInitialized()) { 76 return rootView 77 } 78 79 rootView = super.createRootView().apply { setIsFabric(fabricEnabled) } 80 81 return rootView 82 } 83 84 override fun createRootView(bundle: Bundle?): ReactRootView { 85 if (rootViewWasInitialized()) { 86 return rootView 87 } 88 89 rootView = super.createRootView(bundle) 90 91 return rootView 92 } 93 } 94 } 95 onKeyUpnull96 override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { 97 return if (keyCode == KeyEvent.KEYCODE_MENU || DevMenuManager.onKeyEvent(keyCode, event)) { 98 DevMenuManager.closeMenu() 99 true 100 } else { 101 super.onKeyUp(keyCode, event) 102 } 103 } 104 onPausenull105 override fun onPause() { 106 super.onPause() 107 overridePendingTransition(0, 0) 108 } 109 onStartnull110 override fun onStart() { 111 super.onStart() 112 val instanceManager = DevMenuManager.delegate?.reactInstanceManager() ?: return 113 val supportsDevelopment = DevMenuManager.delegate?.supportsDevelopment() ?: false 114 115 if (supportsDevelopment) { 116 val devSupportManager: DevSupportManager = 117 ReactInstanceManager::class.java.getPrivateDeclaredFieldValue( 118 "mDevSupportManager", instanceManager 119 ) 120 121 devSupportManager.devSupportEnabled = true 122 } 123 } 124 setContentViewnull125 override fun setContentView(view: View?) { 126 super.setContentView(R.layout.bottom_sheet) 127 128 val mainLayout = findViewById<CoordinatorLayout>(R.id.main_layout) 129 val bottomSheet = findViewById<FrameLayout>(R.id.bottom_sheet) 130 bottomSheet.addView(view) 131 132 BottomSheetBehavior.from(bottomSheet).apply { 133 addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { 134 override fun onStateChanged(bottomSheet: View, newState: Int) { 135 if (newState == BottomSheetBehavior.STATE_HIDDEN || newState == BottomSheetBehavior.STATE_COLLAPSED) { 136 DevMenuManager.hideMenu() 137 } 138 } 139 140 override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit 141 }) 142 143 bottomSheet.doOnLayout { 144 state = BottomSheetBehavior.STATE_HALF_EXPANDED 145 } 146 147 mainLayout.setOnClickListener { 148 state = BottomSheetBehavior.STATE_HIDDEN 149 } 150 } 151 } 152 closeBottomSheetnull153 fun closeBottomSheet() { 154 val bottomSheet = findViewById<FrameLayout>(R.id.bottom_sheet) 155 BottomSheetBehavior.from(bottomSheet).state = BottomSheetBehavior.STATE_HIDDEN 156 } 157 158 companion object { 159 var appWasLoaded = false 160 private lateinit var rootView: ReactRootView 161 rootViewWasInitializednull162 private fun rootViewWasInitialized() = ::rootView.isInitialized 163 } 164 } 165