Skip to content
Closed
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
Expand Up @@ -15,6 +15,7 @@
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Window;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.common.logging.FLog;
Expand All @@ -25,6 +26,7 @@
import com.facebook.react.bridge.queue.MessageQueueThread;
import com.facebook.react.bridge.queue.ReactQueueConfiguration;
import com.facebook.react.common.LifecycleState;
import com.facebook.react.interfaces.ExtraWindowEventListener;
import com.facebook.react.turbomodule.core.interfaces.CallInvokerHolder;
import java.lang.ref.WeakReference;
import java.util.Collection;
Expand All @@ -48,6 +50,8 @@ public interface RCTDeviceEventEmitter extends JavaScriptModule {
new CopyOnWriteArraySet<>();
private final CopyOnWriteArraySet<ActivityEventListener> mActivityEventListeners =
new CopyOnWriteArraySet<>();
private final CopyOnWriteArraySet<ExtraWindowEventListener> mExtraWindowEventListeners =
new CopyOnWriteArraySet<>();
private final CopyOnWriteArraySet<WindowFocusChangeListener> mWindowFocusEventListeners =
new CopyOnWriteArraySet<>();
private final ScrollEndedListeners mScrollEndedListeners = new ScrollEndedListeners();
Expand Down Expand Up @@ -246,6 +250,14 @@ public void removeActivityEventListener(ActivityEventListener listener) {
mActivityEventListeners.remove(listener);
}

public void addExtraWindowEventListener(ExtraWindowEventListener listener) {
mExtraWindowEventListeners.add(listener);
}

public void removeExtraWindowEventListener(ExtraWindowEventListener listener) {
mExtraWindowEventListeners.remove(listener);
}

public void addWindowFocusChangeListener(WindowFocusChangeListener listener) {
mWindowFocusEventListeners.add(listener);
}
Expand Down Expand Up @@ -356,6 +368,30 @@ public void onActivityResult(
}
}

@ThreadConfined(UI)
public void onExtraWindowCreate(Window window) {
UiThreadUtil.assertOnUiThread();
for (ExtraWindowEventListener listener : mExtraWindowEventListeners) {
try {
listener.onExtraWindowCreate(window);
} catch (RuntimeException e) {
handleException(e);
}
}
}

@ThreadConfined(UI)
public void onExtraWindowDestroy(Window window) {
UiThreadUtil.assertOnUiThread();
for (ExtraWindowEventListener listener : mExtraWindowEventListeners) {
try {
listener.onExtraWindowDestroy(window);
} catch (RuntimeException e) {
handleException(e);
}
}
}

@ThreadConfined(UI)
public void onWindowFocusChange(boolean hasFocus) {
UiThreadUtil.assertOnUiThread();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.facebook.react.interfaces

import android.view.Window

/**
* Listener for receiving extra window creation and destruction events.
*
* This allows modules to react to new windows being added or removed, such as Dialog windows
* registered by Modal components. Modules like StatusBarModule can implement this interface to
* apply their configuration to all active windows.
*
* Third-party libraries can both implement this listener and emit window events through
* [ReactContext.onExtraWindowCreate] and [ReactContext.onExtraWindowDestroy].
*/
public interface ExtraWindowEventListener {

/** Called when a new [Window] is created (e.g. a Dialog window for a Modal). */
public fun onExtraWindowCreate(window: Window)

/** Called when a [Window] is destroyed (e.g. on Dialog window dismiss). */
public fun onExtraWindowDestroy(window: Window)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ package com.facebook.react.uimanager

import android.app.Activity
import android.content.Context
import android.view.Window
import com.facebook.react.bridge.Callback
import com.facebook.react.bridge.CatalystInstance
import com.facebook.react.bridge.JavaScriptContextHolder
Expand All @@ -22,6 +23,7 @@ import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.ScrollEndedListeners
import com.facebook.react.bridge.UIManager
import com.facebook.react.common.annotations.internal.LegacyArchitecture
import com.facebook.react.interfaces.ExtraWindowEventListener
import com.facebook.react.turbomodule.core.interfaces.CallInvokerHolder

/**
Expand Down Expand Up @@ -67,6 +69,22 @@ public class ThemedReactContext(
reactApplicationContext.removeLifecycleEventListener(listener)
}

override fun addExtraWindowEventListener(listener: ExtraWindowEventListener) {
reactApplicationContext.addExtraWindowEventListener(listener)
}

override fun removeExtraWindowEventListener(listener: ExtraWindowEventListener) {
reactApplicationContext.removeExtraWindowEventListener(listener)
}

override fun onExtraWindowCreate(window: Window) {
reactApplicationContext.onExtraWindowCreate(window)
}

override fun onExtraWindowDestroy(window: Window) {
reactApplicationContext.onExtraWindowDestroy(window)
}

override fun hasCurrentActivity(): Boolean = reactApplicationContext.hasCurrentActivity()

override fun getCurrentActivity(): Activity? = reactApplicationContext.getCurrentActivity()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,9 @@ public class ReactModalHostView(context: ThemedReactContext) :

dialog?.let { nonNullDialog ->
if (nonNullDialog.isShowing) {
nonNullDialog.window?.let { window ->
(context as ThemedReactContext).onExtraWindowDestroy(window)
}
val dialogContext =
ContextUtils.findContextOfType(nonNullDialog.context, Activity::class.java)
if (dialogContext == null || !dialogContext.isFinishing) {
Expand Down Expand Up @@ -341,6 +344,7 @@ public class ReactModalHostView(context: ThemedReactContext) :
newDialog.show()
updateSystemAppearance()
window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
(context as ThemedReactContext).onExtraWindowCreate(window)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.facebook.react.interfaces

import android.view.Window
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactTestHelper
import com.facebook.testutils.shadows.ShadowSoLoader
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(shadows = [ShadowSoLoader::class])
class ExtraWindowEventListenerTest {
private lateinit var reactContext: ReactApplicationContext
private lateinit var window: Window

@Before
fun setUp() {
reactContext = ReactTestHelper.createCatalystContextForTest()
window = mock()
}

@Test
fun testOnExtraWindowCreateNotifiesListener() {
val listener: ExtraWindowEventListener = mock()

reactContext.addExtraWindowEventListener(listener)
reactContext.onExtraWindowCreate(window)

verify(listener, times(1)).onExtraWindowCreate(window)
}

@Test
fun testOnExtraWindowDestroyNotifiesListener() {
val listener: ExtraWindowEventListener = mock()

reactContext.addExtraWindowEventListener(listener)
reactContext.onExtraWindowDestroy(window)

verify(listener, times(1)).onExtraWindowDestroy(window)
}

@Test
fun testMultipleListenersAreNotified() {
val listener1: ExtraWindowEventListener = mock()
val listener2: ExtraWindowEventListener = mock()

reactContext.addExtraWindowEventListener(listener1)
reactContext.addExtraWindowEventListener(listener2)
reactContext.onExtraWindowCreate(window)

verify(listener1, times(1)).onExtraWindowCreate(window)
verify(listener2, times(1)).onExtraWindowCreate(window)
}

@Test
fun testRemovedListenerIsNotNotified() {
val listener: ExtraWindowEventListener = mock()

reactContext.addExtraWindowEventListener(listener)
reactContext.removeExtraWindowEventListener(listener)
reactContext.onExtraWindowCreate(window)

verify(listener, never()).onExtraWindowCreate(window)
}

@Test
fun testOnlyRemovedListenerStopsReceivingEvents() {
val listener1: ExtraWindowEventListener = mock()
val listener2: ExtraWindowEventListener = mock()

reactContext.addExtraWindowEventListener(listener1)
reactContext.addExtraWindowEventListener(listener2)
reactContext.removeExtraWindowEventListener(listener1)
reactContext.onExtraWindowDestroy(window)

verify(listener1, never()).onExtraWindowDestroy(window)
verify(listener2, times(1)).onExtraWindowDestroy(window)
}

@Test
fun testListenerReceivesBothCreateAndDestroyEvents() {
val listener: ExtraWindowEventListener = mock()

reactContext.addExtraWindowEventListener(listener)
reactContext.onExtraWindowCreate(window)
reactContext.onExtraWindowDestroy(window)

verify(listener, times(1)).onExtraWindowCreate(window)
verify(listener, times(1)).onExtraWindowDestroy(window)
}

@Test
fun testNoListenersDoesNotCrash() {
// Should not throw when no listeners are registered
reactContext.onExtraWindowCreate(window)
reactContext.onExtraWindowDestroy(window)
}

@Test
fun testDuplicateAddIsIdempotent() {
val listener: ExtraWindowEventListener = mock()

reactContext.addExtraWindowEventListener(listener)
reactContext.addExtraWindowEventListener(listener)
reactContext.onExtraWindowCreate(window)

// CopyOnWriteArraySet deduplicates, so listener should only be called once
verify(listener, times(1)).onExtraWindowCreate(window)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.facebook.react.uimanager

import android.view.Window
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactTestHelper
import com.facebook.react.interfaces.ExtraWindowEventListener
import com.facebook.testutils.shadows.ShadowSoLoader
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(shadows = [ShadowSoLoader::class])
class ThemedReactContextTest {
private lateinit var reactApplicationContext: ReactApplicationContext
private lateinit var themedReactContext: ThemedReactContext
private lateinit var window: Window

@Before
fun setUp() {
reactApplicationContext = ReactTestHelper.createCatalystContextForTest()
themedReactContext =
ThemedReactContext(reactApplicationContext, RuntimeEnvironment.getApplication(), null, -1)
window = mock()
}

@Test
fun testAddExtraWindowEventListenerDelegatesToReactApplicationContext() {
val listener: ExtraWindowEventListener = mock()

themedReactContext.addExtraWindowEventListener(listener)
// Verify the listener was registered on the underlying context by dispatching an event
reactApplicationContext.onExtraWindowCreate(window)

verify(listener, times(1)).onExtraWindowCreate(window)
}

@Test
fun testRemoveExtraWindowEventListenerDelegatesToReactApplicationContext() {
val listener: ExtraWindowEventListener = mock()

themedReactContext.addExtraWindowEventListener(listener)
themedReactContext.removeExtraWindowEventListener(listener)
// After removal via ThemedReactContext, the listener should not be notified
reactApplicationContext.onExtraWindowCreate(window)

verify(listener, times(0)).onExtraWindowCreate(window)
}

@Test
fun testOnExtraWindowCreateDelegatesToReactApplicationContext() {
val listener: ExtraWindowEventListener = mock()

reactApplicationContext.addExtraWindowEventListener(listener)
// Dispatching via ThemedReactContext should reach listeners on the underlying context
themedReactContext.onExtraWindowCreate(window)

verify(listener, times(1)).onExtraWindowCreate(window)
}

@Test
fun testOnExtraWindowDestroyDelegatesToReactApplicationContext() {
val listener: ExtraWindowEventListener = mock()

reactApplicationContext.addExtraWindowEventListener(listener)
// Dispatching via ThemedReactContext should reach listeners on the underlying context
themedReactContext.onExtraWindowDestroy(window)

verify(listener, times(1)).onExtraWindowDestroy(window)
}
}
Loading