Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/expo-audio/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -180,22 +176,18 @@ class AudioPlayer(
intendedPlayingState = false
}

playerScope.launch {
val updateMap = mutableMapOf<String, Any?>(
"playbackState" to playbackStateToString(playbackState)
)
if (justFinished) {
updateMap["didJustFinish"] = true
updateMap["playing"] = false
}
sendPlayerUpdate(updateMap)
val updateMap = mutableMapOf<String, Any?>(
"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(
Expand All @@ -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)))
}
}
})
Expand Down Expand Up @@ -262,12 +252,11 @@ class AudioPlayer(
)
}

private suspend fun sendPlayerUpdate(map: Map<String, Any?>? = null) =
withContext(Dispatchers.Main) {
val data = currentStatus()
val body = map?.let { data + it } ?: data
emit(PLAYBACK_STATUS_UPDATE, body)
}
private fun sendPlayerUpdate(map: Map<String, Any?>? = null) {
val data = currentStatus()
val body = map?.let { data + it } ?: data
emit(PLAYBACK_STATUS_UPDATE, body)
}

private fun sendAudioSampleUpdate(sample: List<Float>) {
val body = mapOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -98,11 +97,7 @@ class ErrorRecovery(
}

private fun registerErrorHandler(devSupportManager: DevSupportManager) {
if (enableBridgelessArchitecture) {
registerErrorHandlerImplBridgeless()
} else {
registerErrorHandlerImplBridge(devSupportManager)
}
registerErrorHandlerImplBridgeless()
}

private fun registerErrorHandlerImplBridgeless() {
Expand All @@ -128,11 +123,7 @@ class ErrorRecovery(
}

private fun unregisterErrorHandler() {
if (enableBridgelessArchitecture) {
unregisterErrorHandlerImplBridgeless()
} else {
unregisterErrorHandlerImplBridge()
}
unregisterErrorHandlerImplBridgeless()
}

private fun unregisterErrorHandlerImplBridgeless() {
Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -50,8 +47,6 @@ class RelaunchProcedure(

procedureContext.processStateEvent(UpdatesStateEvent.Restart())

val oldLaunchAssetFile = getCurrentLauncher().launchAssetFile

val newLauncher = DatabaseLauncher(
context,
updatesConfiguration,
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Loading