From ce8a7bf3ac6311e565f814ed5d7fe6eba946816a Mon Sep 17 00:00:00 2001 From: Alan Hughes <30924086+alanjhughes@users.noreply.github.com> Date: Mon, 16 Feb 2026 09:24:24 +0000 Subject: [PATCH 1/2] [android][audio] Improve event handling (#43121) --- packages/expo-audio/CHANGELOG.md | 2 + .../java/expo/modules/audio/AudioPlayer.kt | 47 +++++++------------ 2 files changed, 20 insertions(+), 29 deletions(-) diff --git a/packages/expo-audio/CHANGELOG.md b/packages/expo-audio/CHANGELOG.md index 8e3856a35d7162..53cc410f7e549c 100644 --- a/packages/expo-audio/CHANGELOG.md +++ b/packages/expo-audio/CHANGELOG.md @@ -16,6 +16,8 @@ ### 💡 Others +- [Android] Improve event handling. ([#43121](https://github.com/expo/expo/pull/43121) by [@alanjhughes](https://github.com/alanjhughes)) + ## 55.0.5 — 2026-02-08 _This version does not introduce any user-facing changes._ diff --git a/packages/expo-audio/android/src/main/java/expo/modules/audio/AudioPlayer.kt b/packages/expo-audio/android/src/main/java/expo/modules/audio/AudioPlayer.kt index 60bc47196baa36..a9ccec7164e1ad 100644 --- a/packages/expo-audio/android/src/main/java/expo/modules/audio/AudioPlayer.kt +++ b/packages/expo-audio/android/src/main/java/expo/modules/audio/AudioPlayer.kt @@ -29,7 +29,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext + import java.util.UUID private const val PLAYBACK_STATUS_UPDATE = "playbackStatusUpdate" @@ -62,7 +62,7 @@ class AudioPlayer( var isActiveForLockScreen = false private var metadata: Metadata? = null - private var playerScope = CoroutineScope(Dispatchers.Default) + private var playerScope = CoroutineScope(Dispatchers.Main) private var samplingEnabled = false private var visualizer: Visualizer? = null private var playing = false @@ -160,15 +160,11 @@ class AudioPlayer( if (isTransient) { return } - playerScope.launch { - sendPlayerUpdate(mapOf("playing" to isPlaying)) - } + sendPlayerUpdate(mapOf("playing" to isPlaying)) } override fun onIsLoadingChanged(isLoading: Boolean) { - playerScope.launch { - sendPlayerUpdate(mapOf("isLoaded" to !isLoading)) - } + sendPlayerUpdate() } override fun onPlaybackStateChanged(playbackState: Int) { @@ -180,22 +176,18 @@ class AudioPlayer( intendedPlayingState = false } - playerScope.launch { - val updateMap = mutableMapOf( - "playbackState" to playbackStateToString(playbackState) - ) - if (justFinished) { - updateMap["didJustFinish"] = true - updateMap["playing"] = false - } - sendPlayerUpdate(updateMap) + val updateMap = mutableMapOf( + "playbackState" to playbackStateToString(playbackState) + ) + if (justFinished) { + updateMap["didJustFinish"] = true + updateMap["playing"] = false } + sendPlayerUpdate(updateMap) } override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { - playerScope.launch { - sendPlayerUpdate() - } + sendPlayerUpdate() } override fun onPositionDiscontinuity( @@ -204,9 +196,7 @@ class AudioPlayer( reason: Int ) { if (reason == Player.DISCONTINUITY_REASON_SEEK) { - playerScope.launch { - sendPlayerUpdate(mapOf("currentTime" to (newPosition.positionMs / 1000f))) - } + sendPlayerUpdate(mapOf("currentTime" to (newPosition.positionMs / 1000f))) } } }) @@ -262,12 +252,11 @@ class AudioPlayer( ) } - private suspend fun sendPlayerUpdate(map: Map? = null) = - withContext(Dispatchers.Main) { - val data = currentStatus() - val body = map?.let { data + it } ?: data - emit(PLAYBACK_STATUS_UPDATE, body) - } + private fun sendPlayerUpdate(map: Map? = null) { + val data = currentStatus() + val body = map?.let { data + it } ?: data + emit(PLAYBACK_STATUS_UPDATE, body) + } private fun sendAudioSampleUpdate(sample: List) { val body = mapOf( From 9b47d02e765d0da8fea3c0d6c71dda47199074a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kosmaty?= Date: Mon, 16 Feb 2026 10:34:49 +0100 Subject: [PATCH 2/2] [updates][Android] Remove usage of `enableBridgelessArchitecture` (#42905) --- .../errorrecovery/ErrorRecoveryTest.kt | 4 +- .../updates/errorrecovery/ErrorRecovery.kt | 19 ++------ .../updates/procedures/RelaunchProcedure.kt | 44 +------------------ .../procedures/RestartReactAppExtensions.kt | 16 +++---- .../updates/procedures/StartupProcedure.kt | 3 +- 5 files changed, 12 insertions(+), 74 deletions(-) diff --git a/packages/expo-updates/android/src/androidTest/java/expo/modules/updates/errorrecovery/ErrorRecoveryTest.kt b/packages/expo-updates/android/src/androidTest/java/expo/modules/updates/errorrecovery/ErrorRecoveryTest.kt index 1f6d581efc65f6..01b5ce39c6cc06 100644 --- a/packages/expo-updates/android/src/androidTest/java/expo/modules/updates/errorrecovery/ErrorRecoveryTest.kt +++ b/packages/expo-updates/android/src/androidTest/java/expo/modules/updates/errorrecovery/ErrorRecoveryTest.kt @@ -16,12 +16,12 @@ class ErrorRecoveryTest { private var mockDelegate: ErrorRecoveryDelegate = mockk() private val context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext private val updatesLogger = UpdatesLogger(context.filesDir) - private var errorRecovery: ErrorRecovery = ErrorRecovery(updatesLogger, enableBridgelessArchitecture = true) + private var errorRecovery: ErrorRecovery = ErrorRecovery(updatesLogger) @Before fun setup() { mockDelegate = mockk(relaxed = true) - errorRecovery = ErrorRecovery(updatesLogger, enableBridgelessArchitecture = true) + errorRecovery = ErrorRecovery(updatesLogger) errorRecovery.initialize(mockDelegate) errorRecovery.handler = spyk(ErrorRecoveryHandler(errorRecovery.handlerThread.looper, mockDelegate, UpdatesLogger(context.filesDir))) // make handler run synchronously diff --git a/packages/expo-updates/android/src/main/java/expo/modules/updates/errorrecovery/ErrorRecovery.kt b/packages/expo-updates/android/src/main/java/expo/modules/updates/errorrecovery/ErrorRecovery.kt index d87a9ffa40fc3d..21fda0fda01bab 100644 --- a/packages/expo-updates/android/src/main/java/expo/modules/updates/errorrecovery/ErrorRecovery.kt +++ b/packages/expo-updates/android/src/main/java/expo/modules/updates/errorrecovery/ErrorRecovery.kt @@ -27,8 +27,7 @@ import java.lang.ref.WeakReference * and so there is no more need to trigger the error recovery pipeline. */ class ErrorRecovery( - private val logger: UpdatesLogger, - private val enableBridgelessArchitecture: Boolean = true + private val logger: UpdatesLogger ) { internal val handlerThread = HandlerThread("expo-updates-error-recovery") internal lateinit var handler: Handler @@ -98,11 +97,7 @@ class ErrorRecovery( } private fun registerErrorHandler(devSupportManager: DevSupportManager) { - if (enableBridgelessArchitecture) { - registerErrorHandlerImplBridgeless() - } else { - registerErrorHandlerImplBridge(devSupportManager) - } + registerErrorHandlerImplBridgeless() } private fun registerErrorHandlerImplBridgeless() { @@ -128,11 +123,7 @@ class ErrorRecovery( } private fun unregisterErrorHandler() { - if (enableBridgelessArchitecture) { - unregisterErrorHandlerImplBridgeless() - } else { - unregisterErrorHandlerImplBridge() - } + unregisterErrorHandlerImplBridgeless() } private fun unregisterErrorHandlerImplBridgeless() { @@ -160,8 +151,4 @@ class ErrorRecovery( // a future time, so delay for a few more seconds in case there are any scheduled messages handler.postDelayed({ handlerThread.quitSafely() }, 10000) } - - companion object { - private val TAG = ErrorRecovery::class.java.simpleName - } } diff --git a/packages/expo-updates/android/src/main/java/expo/modules/updates/procedures/RelaunchProcedure.kt b/packages/expo-updates/android/src/main/java/expo/modules/updates/procedures/RelaunchProcedure.kt index 64cc2870d28557..4cbb7670f0a185 100644 --- a/packages/expo-updates/android/src/main/java/expo/modules/updates/procedures/RelaunchProcedure.kt +++ b/packages/expo-updates/android/src/main/java/expo/modules/updates/procedures/RelaunchProcedure.kt @@ -3,9 +3,6 @@ package expo.modules.updates.procedures import android.app.Activity import android.content.Context import com.facebook.react.ReactApplication -import com.facebook.react.bridge.JSBundleLoader -import expo.modules.core.interfaces.ReactNativeHostHandler -import expo.modules.rncompatibility.ReactNativeFeatureFlags import expo.modules.updates.UpdatesConfiguration import expo.modules.updates.db.DatabaseHolder import expo.modules.updates.db.Reaper @@ -14,9 +11,9 @@ import expo.modules.updates.launcher.Launcher import expo.modules.updates.loader.FileDownloader import expo.modules.updates.logging.UpdatesErrorCode import expo.modules.updates.logging.UpdatesLogger +import expo.modules.updates.reloadscreen.ReloadScreenManager import expo.modules.updates.selectionpolicy.SelectionPolicy import expo.modules.updates.statemachine.UpdatesStateEvent -import expo.modules.updates.reloadscreen.ReloadScreenManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -50,8 +47,6 @@ class RelaunchProcedure( procedureContext.processStateEvent(UpdatesStateEvent.Restart()) - val oldLaunchAssetFile = getCurrentLauncher().launchAssetFile - val newLauncher = DatabaseLauncher( context, updatesConfiguration, @@ -71,14 +66,6 @@ class RelaunchProcedure( } setCurrentLauncher(newLauncher) - val newLaunchAssetFile = getCurrentLauncher().launchAssetFile - if (newLaunchAssetFile != null && newLaunchAssetFile != oldLaunchAssetFile) { - try { - replaceLaunchAssetFileIfNeeded(reactApplication, newLaunchAssetFile) - } catch (e: Exception) { - logger.error("Could not reset launchAssetFile for the ReactApplication", e, UpdatesErrorCode.Unknown) - } - } callback.onSuccess() procedureScope.launch { @@ -114,33 +101,4 @@ class RelaunchProcedure( private suspend fun launchWith(newLauncher: DatabaseLauncher) { newLauncher.launch(databaseHolder.database) } - - /** - * For bridgeless mode, the restarting will pull the new [JSBundleLoader] - * based on the new [DatabaseLauncher] through the [ReactNativeHostHandler]. - * So this method is a no-op for bridgeless mode. - * - * For bridge mode unfortunately, even though RN exposes a way to reload an application, - * it assumes that the JS bundle will stay at the same location throughout - * the entire lifecycle of the app. To change the location of the bundle, - * we need to use reflection to set an inaccessible field in the - * [com.facebook.react.ReactInstanceManager]. - */ - private fun replaceLaunchAssetFileIfNeeded( - reactApplication: ReactApplication, - launchAssetFile: String - ) { - if (ReactNativeFeatureFlags.enableBridgelessArchitecture) { - return - } - - val instanceManager = reactApplication.reactNativeHost.reactInstanceManager - val jsBundleLoaderField = instanceManager.javaClass.getDeclaredField("mBundleLoader") - jsBundleLoaderField.isAccessible = true - jsBundleLoaderField[instanceManager] = JSBundleLoader.createFileLoader(launchAssetFile) - } - - companion object { - private val TAG = RelaunchProcedure::class.java.simpleName - } } diff --git a/packages/expo-updates/android/src/main/java/expo/modules/updates/procedures/RestartReactAppExtensions.kt b/packages/expo-updates/android/src/main/java/expo/modules/updates/procedures/RestartReactAppExtensions.kt index 25c6e804d31f9e..7392ca808be584 100644 --- a/packages/expo-updates/android/src/main/java/expo/modules/updates/procedures/RestartReactAppExtensions.kt +++ b/packages/expo-updates/android/src/main/java/expo/modules/updates/procedures/RestartReactAppExtensions.kt @@ -3,7 +3,6 @@ package expo.modules.updates.procedures import android.app.Activity import com.facebook.react.ReactApplication import com.facebook.react.common.LifecycleState -import expo.modules.rncompatibility.ReactNativeFeatureFlags /** * An extension for [ReactApplication] to restart the app @@ -12,15 +11,10 @@ import expo.modules.rncompatibility.ReactNativeFeatureFlags * @param reason The restart reason. Only used on bridgeless mode. */ internal fun ReactApplication.restart(activity: Activity?, reason: String) { - if (ReactNativeFeatureFlags.enableBridgelessArchitecture) { - val reactHost = this.reactHost - check(reactHost != null) - if (reactHost.lifecycleState != LifecycleState.RESUMED && activity != null) { - reactHost.onHostResume(activity) - } - reactHost.reload(reason) - return + val reactHost = this.reactHost + check(reactHost != null) + if (reactHost.lifecycleState != LifecycleState.RESUMED && activity != null) { + reactHost.onHostResume(activity) } - - reactNativeHost.reactInstanceManager.recreateReactContextInBackground() + reactHost.reload(reason) } diff --git a/packages/expo-updates/android/src/main/java/expo/modules/updates/procedures/StartupProcedure.kt b/packages/expo-updates/android/src/main/java/expo/modules/updates/procedures/StartupProcedure.kt index a7400bf080fc12..9585d69a1afd5c 100644 --- a/packages/expo-updates/android/src/main/java/expo/modules/updates/procedures/StartupProcedure.kt +++ b/packages/expo-updates/android/src/main/java/expo/modules/updates/procedures/StartupProcedure.kt @@ -2,7 +2,6 @@ package expo.modules.updates.procedures import android.content.Context import com.facebook.react.devsupport.interfaces.DevSupportManager -import expo.modules.rncompatibility.ReactNativeFeatureFlags import expo.modules.updates.UpdatesConfiguration import expo.modules.updates.db.DatabaseHolder import expo.modules.updates.db.entity.AssetEntity @@ -66,7 +65,7 @@ class StartupProcedure( var emergencyLaunchException: Exception? = null private set - private val errorRecovery = ErrorRecovery(logger, ReactNativeFeatureFlags.enableBridgelessArchitecture) + private val errorRecovery = ErrorRecovery(logger) private var remoteLoadStatus = ErrorRecoveryDelegate.RemoteLoadStatus.IDLE private val loaderTask = LoaderTask(