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