Skip to content
Open
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
@@ -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.bridge

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 @@ -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 Down Expand Up @@ -48,6 +49,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 +249,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 +367,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
Expand Up @@ -9,12 +9,14 @@ package com.facebook.react.modules.statusbar

import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.os.Build
import android.view.View
import android.view.WindowInsetsController
import android.view.Window
import android.view.WindowManager
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import com.facebook.common.logging.FLog
import com.facebook.fbreact.specs.NativeStatusBarManagerAndroidSpec
import com.facebook.react.bridge.ExtraWindowEventListener
import com.facebook.react.bridge.GuardedRunnable
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
Expand All @@ -24,13 +26,43 @@ import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.DisplayMetricsHolder.getStatusBarHeightPx
import com.facebook.react.uimanager.PixelUtil
import com.facebook.react.views.view.isEdgeToEdgeFeatureFlagOn
import com.facebook.react.views.view.setStatusBarStyle
import com.facebook.react.views.view.setStatusBarTranslucency
import com.facebook.react.views.view.setStatusBarVisibility

/** [NativeModule] that allows changing the appearance of the status bar. */
@ReactModule(name = NativeStatusBarManagerAndroidSpec.NAME)
internal class StatusBarModule(reactContext: ReactApplicationContext?) :
NativeStatusBarManagerAndroidSpec(reactContext) {
NativeStatusBarManagerAndroidSpec(reactContext), ExtraWindowEventListener {

private val extraWindows = mutableSetOf<Window>()

init {
reactApplicationContext.addExtraWindowEventListener(this)
}

override fun invalidate() {
super.invalidate()
reactApplicationContext.removeExtraWindowEventListener(this)
}

override fun onExtraWindowCreate(window: Window) {
extraWindows.add(window)

UiThreadUtil.runOnUiThread {
val controller = WindowCompat.getInsetsController(window, window.decorView)
val insets = ViewCompat.getRootWindowInsets(window.decorView)
val style = if (controller.isAppearanceLightStatusBars) "dark-content" else "light-content"
val visible = insets?.isVisible(WindowInsetsCompat.Type.statusBars()) ?: true

window.setStatusBarStyle(style)
window.setStatusBarVisibility(!visible)
}
}

override fun onExtraWindowDestroy(window: Window) {
extraWindows.remove(window)
}

@Suppress("DEPRECATION")
override fun getTypedExportedConstants(): Map<String, Any> {
Expand Down Expand Up @@ -118,10 +150,12 @@ internal class StatusBarModule(reactContext: ReactApplicationContext?) :
)
return
}
UiThreadUtil.runOnUiThread { activity.window?.setStatusBarVisibility(hidden) }
UiThreadUtil.runOnUiThread {
activity.window?.setStatusBarVisibility(hidden)
extraWindows.forEach { it.setStatusBarVisibility(hidden) }
}
}

@Suppress("DEPRECATION")
override fun setStyle(style: String?) {
val activity = reactApplicationContext.getCurrentActivity()
if (activity == null) {
Expand All @@ -131,36 +165,10 @@ internal class StatusBarModule(reactContext: ReactApplicationContext?) :
)
return
}
UiThreadUtil.runOnUiThread(
Runnable {
val window = activity.window ?: return@Runnable
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
val insetsController = window.insetsController ?: return@Runnable
if ("dark-content" == style) {
// dark-content means dark icons on a light status bar
insetsController.setSystemBarsAppearance(
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
)
} else {
insetsController.setSystemBarsAppearance(
0,
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
)
}
} else {
val decorView = window.decorView
var systemUiVisibilityFlags = decorView.systemUiVisibility
systemUiVisibilityFlags =
if ("dark-content" == style) {
systemUiVisibilityFlags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
} else {
systemUiVisibilityFlags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
}
decorView.systemUiVisibility = systemUiVisibilityFlags
}
}
)
UiThreadUtil.runOnUiThread {
activity.window?.setStatusBarStyle(style)
extraWindows.forEach { it.setStatusBarStyle(style) }
}
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ 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.ExtraWindowEventListener
import com.facebook.react.bridge.JavaScriptContextHolder
import com.facebook.react.bridge.JavaScriptModule
import com.facebook.react.bridge.LifecycleEventListener
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 @@ -196,6 +196,9 @@ public class ReactModalHostView(context: ThemedReactContext) :
UiThreadUtil.assertOnUiThread()

dialog?.let { nonNullDialog ->
nonNullDialog.window?.let { window ->
(context as ThemedReactContext).onExtraWindowDestroy(window)
}
if (nonNullDialog.isShowing) {
val dialogContext =
ContextUtils.findContextOfType(nonNullDialog.context, Activity::class.java)
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
Expand Up @@ -9,7 +9,9 @@ package com.facebook.react.views.view

import android.graphics.Color
import android.os.Build
import android.view.View
import android.view.Window
import android.view.WindowInsetsController
import android.view.WindowManager
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
Expand Down Expand Up @@ -65,6 +67,33 @@ internal fun Window.setStatusBarVisibility(isHidden: Boolean) {
}
}

@Suppress("DEPRECATION")
internal fun Window.setStatusBarStyle(style: String?) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
if ("dark-content" == style) {
// dark-content means dark icons on a light status bar
insetsController?.setSystemBarsAppearance(
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
)
} else {
insetsController?.setSystemBarsAppearance(
0,
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
)
}
} else {
var systemUiVisibilityFlags = decorView.systemUiVisibility
systemUiVisibilityFlags =
if ("dark-content" == style) {
systemUiVisibilityFlags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
} else {
systemUiVisibilityFlags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
}
decorView.systemUiVisibility = systemUiVisibilityFlags
}
}

@Suppress("DEPRECATION")
private fun Window.statusBarHide() {
if (isEdgeToEdgeFeatureFlagOn) {
Expand Down
Loading