const { AndroidConfig, withAndroidManifest, withMainActivity } = require('expo/config-plugins') // Why: One UI gives a split-screen pane its own density (#36305). Without `density` in // configChanges Android recreates MainActivity, closing the open session and orphaning the image // picker. Keeping the activity is not enough: React Native seeds its DisplayMetricsHolder from the // application context, which keeps the full-screen density, so the pane is laid out at the wrong // scale. MainActivity re-seeds it from its own resources after React Native does. const MARKER = 'orca: window density' const CLASS_HEADER = 'class MainActivity : ReactActivity() {' const IMPORTS = [ 'android.content.res.Configuration', 'android.os.Bundle', 'android.util.DisplayMetrics', 'com.facebook.react.uimanager.DisplayMetricsHolder' ] const METHODS = ` // ${MARKER}, generated by plugins/android-window-density.js. // React Native re-seeds from the application context when it creates the root view (onCreate) // and in onConfigurationChanged, so these run before the next measure. Its root view's layout // listener also re-seeds whenever the rotation differs from the last one it saw, after measuring; // the pre-draw check runs after every layout listener and asks for another pass, in which React // Native re-sends the window size to JS. override fun onPostCreate(savedInstanceState: Bundle?) { super.onPostCreate(savedInstanceState) window.decorView.viewTreeObserver.addOnPreDrawListener { val held = DisplayMetricsHolder.getScreenDisplayMetrics().densityDpi if (held != resources.displayMetrics.densityDpi) { useWindowDensity() window.decorView.requestLayout() } true } } override fun onResume() { super.onResume() useWindowDensity() } override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) useWindowDensity() } // React Native's initDisplayMetrics with the pane's density on the real display size, built before // it is published because text measurement reads it off the UI thread. @Suppress("DEPRECATION") private fun useWindowDensity() { val pane = resources.displayMetrics val screen = DisplayMetrics().apply { windowManager.defaultDisplay.getRealMetrics(this) density = pane.density densityDpi = pane.densityDpi scaledDensity = pane.scaledDensity } DisplayMetricsHolder.setWindowDisplayMetrics(pane) DisplayMetricsHolder.setScreenDisplayMetrics(screen) } ` function patchMainActivity(contents) { if (contents.includes(MARKER)) { return contents } // A changed template must fail prebuild rather than drop or double an override. if ( !contents.includes(CLASS_HEADER) || !contents.trimEnd().endsWith('}') || /fun (onPostCreate|onResume|onConfigurationChanged)\(/.test(contents) ) { throw new Error( "android-window-density: Expo's MainActivity template changed; update the plugin before building." ) } const imports = IMPORTS.filter((name) => !contents.includes(`import ${name}\n`)) .map((name) => `import ${name}\n`) .join('') const body = contents.trimEnd().slice(0, -1).trimEnd() return `${body.replace(/^(package [^\n]+\n)/, `$1${imports}`)}\n${METHODS}}\n` } module.exports = function withAndroidWindowDensity(config) { config = withAndroidManifest(config, (cfg) => { const activity = AndroidConfig.Manifest.getMainActivityOrThrow(cfg.modResults) const changes = (activity.$['android:configChanges'] ?? '').split('|').filter(Boolean) if (!changes.includes('density')) { activity.$['android:configChanges'] = [...changes, 'density'].join('|') } return cfg }) return withMainActivity(config, (cfg) => { if (cfg.modResults.language !== 'kt') { throw new Error( `android-window-density: expected a Kotlin MainActivity, got ${cfg.modResults.language}` ) } cfg.modResults.contents = patchMainActivity(cfg.modResults.contents) return cfg }) }