Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(Android): prevent crash with InsetsObserverProxy already registered #2526

Merged
merged 3 commits into from
Nov 21, 2024
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
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package com.swmansion.rnscreens

import android.util.Log
import android.view.View
import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.facebook.react.bridge.LifecycleEventListener
import com.facebook.react.bridge.ReactApplicationContext
import java.lang.ref.WeakReference

object InsetsObserverProxy : OnApplyWindowInsetsListener {
object InsetsObserverProxy : OnApplyWindowInsetsListener, LifecycleEventListener {
private val listeners: ArrayList<OnApplyWindowInsetsListener> = arrayListOf()
private var eventSourceView: WeakReference<View> = WeakReference(null)

Expand All @@ -15,8 +18,16 @@ object InsetsObserverProxy : OnApplyWindowInsetsListener {
// whether this observer has been initially registered.
private var hasBeenRegistered: Boolean = false

// Mainly debug variable to log warnings in case we missed some code path regarding
// context lifetime handling.
private var isObservingContextLifetime: Boolean = false

private var shouldForwardInsetsToView = true

// Allow only when we have not been registered yet or the view we're observing has been
// invalidated due to some lifecycle we have not observed.
private val allowRegistration get() = !hasBeenRegistered || eventSourceView.get() == null

override fun onApplyWindowInsets(
v: View,
insets: WindowInsetsCompat,
Expand All @@ -34,6 +45,34 @@ object InsetsObserverProxy : OnApplyWindowInsetsListener {
return rollingInsets
}

// Call this method to ensure that the observer proxy is
// unregistered when apps is destroyed or we change activity.
fun registerWithContext(context: ReactApplicationContext) {
if (isObservingContextLifetime) {
Log.w(
"[RNScreens]",
"InsetObserverProxy registers on new context while it has not been invalidated on the old one. Please report this as issue at https://github.com/software-mansion/react-native-screens/issues",
)
}

isObservingContextLifetime = true
context.addLifecycleEventListener(this)
}

override fun onHostResume() = Unit

override fun onHostPause() = Unit

override fun onHostDestroy() {
val observedView = getObservedView()
if (hasBeenRegistered && observedView != null) {
ViewCompat.setOnApplyWindowInsetsListener(observedView, null)
hasBeenRegistered = false
eventSourceView.clear()
}
isObservingContextLifetime = false
}

fun addOnApplyWindowInsetsListener(listener: OnApplyWindowInsetsListener) {
listeners.add(listener)
}
Expand All @@ -42,16 +81,17 @@ object InsetsObserverProxy : OnApplyWindowInsetsListener {
listeners.remove(listener)
}

fun registerOnView(view: View) {
if (!hasBeenRegistered) {
/**
* @return boolean whether the proxy registered as a listener on a view
*/
fun registerOnView(view: View): Boolean {
if (allowRegistration) {
ViewCompat.setOnApplyWindowInsetsListener(view, this)
eventSourceView = WeakReference(view)
hasBeenRegistered = true
} else if (getObservedView() != view) {
throw IllegalStateException(
"[RNScreens] Attempt to register InsetsObserverProxy on $view while it has been already registered on ${getObservedView()}",
)
return true
}
return false
}

fun unregister() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ class RNScreensPackage : TurboReactPackage() {
screenDummyLayoutHelper = ScreenDummyLayoutHelper(reactContext)
}

// Proxy needs to register for lifecycle events in order to unregister itself
// on activity restarts.
InsetsObserverProxy.registerWithContext(reactContext)

return listOf<ViewManager<*, *>>(
ScreenContainerViewManager(),
ScreenViewManager(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ class DimmingFragment(
override fun onStart() {
// This is the earliest we can access child fragment manager & present another fragment
super.onStart()
insetsProxy.registerOnView(requireRootView())
insetsProxy.registerOnView(requireDecorView())
presentNestedFragment()
}

Expand Down Expand Up @@ -307,7 +307,7 @@ class DimmingFragment(
}
}

private fun requireRootView(): View =
private fun requireDecorView(): View =
checkNotNull(screen.reactContext.currentActivity) { "[RNScreens] Attempt to access activity on detached context" }
.window.decorView

Expand Down
Loading