diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index c0890cd56abf72..b48c0bc1000053 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -2059,14 +2059,18 @@ public class com/facebook/react/devsupport/BundleDownloader$BundleInfo { public fun toJSONString ()Ljava/lang/String; } -public class com/facebook/react/devsupport/DefaultDevLoadingViewImplementation : com/facebook/react/devsupport/interfaces/DevLoadingViewManager { +public final class com/facebook/react/devsupport/DefaultDevLoadingViewImplementation : com/facebook/react/devsupport/interfaces/DevLoadingViewManager { + public static final field Companion Lcom/facebook/react/devsupport/DefaultDevLoadingViewImplementation$Companion; public fun (Lcom/facebook/react/devsupport/ReactInstanceDevHelper;)V public fun hide ()V - public static fun setDevLoadingEnabled (Z)V public fun showMessage (Ljava/lang/String;)V public fun updateProgress (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;)V } +public final class com/facebook/react/devsupport/DefaultDevLoadingViewImplementation$Companion { + public final fun setDevLoadingEnabled (Z)V +} + public final class com/facebook/react/devsupport/DefaultDevSupportManagerFactory : com/facebook/react/devsupport/DevSupportManagerFactory { public fun ()V public final fun create (Landroid/content/Context;Lcom/facebook/react/devsupport/ReactInstanceDevHelper;Ljava/lang/String;ZI)Lcom/facebook/react/devsupport/interfaces/DevSupportManager; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DefaultDevLoadingViewImplementation.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DefaultDevLoadingViewImplementation.java deleted file mode 100644 index 74052bb2e91876..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DefaultDevLoadingViewImplementation.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * 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.devsupport; - -import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; -import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; - -import android.app.Activity; -import android.content.Context; -import android.graphics.Rect; -import android.view.Gravity; -import android.view.LayoutInflater; -import android.view.WindowManager.BadTokenException; -import android.widget.PopupWindow; -import android.widget.TextView; -import androidx.annotation.Nullable; -import com.facebook.common.logging.FLog; -import com.facebook.react.R; -import com.facebook.react.bridge.UiThreadUtil; -import com.facebook.react.common.ReactConstants; -import com.facebook.react.devsupport.interfaces.DevLoadingViewManager; -import java.util.Locale; - -/** - * Default implementation of Dev Loading View Manager to display loading messages on top of the - * screen. All methods are thread safe. - */ -public class DefaultDevLoadingViewImplementation implements DevLoadingViewManager { - private static boolean sEnabled = true; - private final ReactInstanceDevHelper mReactInstanceDevHelper; - private @Nullable TextView mDevLoadingView; - private @Nullable PopupWindow mDevLoadingPopup; - - public static void setDevLoadingEnabled(boolean enabled) { - sEnabled = enabled; - } - - public DefaultDevLoadingViewImplementation(ReactInstanceDevHelper reactInstanceManagerHelper) { - mReactInstanceDevHelper = reactInstanceManagerHelper; - } - - @Override - public void showMessage(final String message) { - if (!sEnabled) { - return; - } - - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - showInternal(message); - } - }); - } - - @Override - public void updateProgress( - final @Nullable String status, final @Nullable Integer done, final @Nullable Integer total) { - if (!sEnabled) { - return; - } - - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - StringBuilder message = new StringBuilder(); - message.append(status != null ? status : "Loading"); - if (done != null && total != null && total > 0) { - message.append( - String.format(Locale.getDefault(), " %.1f%%", (float) done / total * 100)); - } - message.append("\u2026"); // `...` character - if (mDevLoadingView != null) { - mDevLoadingView.setText(message); - } - } - }); - } - - @Override - public void hide() { - if (!sEnabled) { - return; - } - - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - hideInternal(); - } - }); - } - - private void showInternal(String message) { - if (mDevLoadingPopup != null && mDevLoadingPopup.isShowing()) { - // already showing - return; - } - - Activity currentActivity = mReactInstanceDevHelper.getCurrentActivity(); - if (currentActivity == null) { - FLog.e( - ReactConstants.TAG, - "Unable to display loading message because react " + "activity isn't available"); - return; - } - - // PopupWindow#showAtLocation uses absolute screen position. In order for - // loading view to be placed below status bar (if the status bar is present) we need to pass - // an appropriate Y offset. - try { - Rect rectangle = new Rect(); - currentActivity.getWindow().getDecorView().getWindowVisibleDisplayFrame(rectangle); - int topOffset = rectangle.top; - - LayoutInflater inflater = - (LayoutInflater) currentActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - - mDevLoadingView = (TextView) inflater.inflate(R.layout.dev_loading_view, null); - mDevLoadingView.setText(message); - - mDevLoadingPopup = new PopupWindow(mDevLoadingView, MATCH_PARENT, WRAP_CONTENT); - mDevLoadingPopup.setTouchable(false); - - mDevLoadingPopup.showAtLocation( - currentActivity.getWindow().getDecorView(), Gravity.NO_GRAVITY, 0, topOffset); - // TODO T164786028: Find out the root cause of the BadTokenException exception here - } catch (BadTokenException e) { - FLog.e( - ReactConstants.TAG, - "Unable to display loading message because react " - + "activity isn't active, message: " - + message); - } - } - - private void hideInternal() { - if (mDevLoadingPopup != null && mDevLoadingPopup.isShowing()) { - mDevLoadingPopup.dismiss(); - mDevLoadingPopup = null; - mDevLoadingView = null; - } - } - - private @Nullable Context getContext() { - return mReactInstanceDevHelper.getCurrentActivity(); - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DefaultDevLoadingViewImplementation.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DefaultDevLoadingViewImplementation.kt new file mode 100644 index 00000000000000..0844b8b725d3bb --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DefaultDevLoadingViewImplementation.kt @@ -0,0 +1,120 @@ +/* + * 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.devsupport + +import android.content.Context +import android.graphics.Rect +import android.view.Gravity +import android.view.LayoutInflater +import android.view.ViewGroup +import android.view.WindowManager +import android.widget.PopupWindow +import android.widget.TextView +import com.facebook.common.logging.FLog +import com.facebook.react.R +import com.facebook.react.bridge.UiThreadUtil +import com.facebook.react.common.ReactConstants +import com.facebook.react.devsupport.interfaces.DevLoadingViewManager +import java.util.Locale + +/** + * Default implementation of Dev Loading View Manager to display loading messages on top of the + * screen. All methods are thread safe. + */ +public class DefaultDevLoadingViewImplementation( + private val reactInstanceDevHelper: ReactInstanceDevHelper +) : DevLoadingViewManager { + private var devLoadingView: TextView? = null + private var devLoadingPopup: PopupWindow? = null + + override fun showMessage(message: String) { + if (!isEnabled) { + return + } + UiThreadUtil.runOnUiThread { showInternal(message) } + } + + override fun updateProgress(status: String?, done: Int?, total: Int?) { + if (!isEnabled) { + return + } + UiThreadUtil.runOnUiThread { + val percentage = + if (done != null && total != null && total > 0) + String.format(Locale.getDefault(), " %.1f%%", done.toFloat() / total * 100) + else "" + devLoadingView?.text = + "${status ?: "Loading"}${percentage}\u2026" // `...` character at the end + } + } + + public override fun hide() { + if (isEnabled) { + UiThreadUtil.runOnUiThread { hideInternal() } + } + } + + private fun showInternal(message: String) { + if (devLoadingPopup?.isShowing == true) { + // already showing + return + } + val currentActivity = reactInstanceDevHelper.currentActivity + if (currentActivity == null) { + FLog.e( + ReactConstants.TAG, + "Unable to display loading message because react " + "activity isn't available") + return + } + + // PopupWindow#showAtLocation uses absolute screen position. In order for + // loading view to be placed below status bar (if the status bar is present) we need to pass + // an appropriate Y offset. + try { + val rectangle = Rect() + currentActivity.window.decorView.getWindowVisibleDisplayFrame(rectangle) + val topOffset = rectangle.top + val inflater = + currentActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + val view = inflater.inflate(R.layout.dev_loading_view, null) as TextView + view.text = message + val popup = + PopupWindow( + view, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + popup.isTouchable = false + popup.showAtLocation(currentActivity.window.decorView, Gravity.NO_GRAVITY, 0, topOffset) + devLoadingView = view + devLoadingPopup = popup + // TODO T164786028: Find out the root cause of the BadTokenException exception here + } catch (e: WindowManager.BadTokenException) { + FLog.e( + ReactConstants.TAG, + "Unable to display loading message because react activity isn't active, message: $message") + } + } + + private fun hideInternal() { + val popup = devLoadingPopup ?: return + if (popup.isShowing == true) { + popup.dismiss() + devLoadingPopup = null + devLoadingView = null + } + } + + private val context: Context? + get() = reactInstanceDevHelper.currentActivity + + public companion object { + private var isEnabled = true + + public fun setDevLoadingEnabled(enabled: Boolean) { + isEnabled = enabled + } + } +}