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
3 changes: 3 additions & 0 deletions packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ public class com/facebook/react/ReactActivityDelegate {
public fun getReactInstanceManager ()Lcom/facebook/react/ReactInstanceManager;
protected fun getReactNativeHost ()Lcom/facebook/react/ReactNativeHost;
protected fun isFabricEnabled ()Z
protected fun isFullyDrawnReportingEnabled ()Z
protected fun isWideColorGamutEnabled ()Z
protected fun loadApp (Ljava/lang/String;)V
public fun onActivityResult (IILandroid/content/Intent;)V
Expand Down Expand Up @@ -138,6 +139,7 @@ public class com/facebook/react/ReactDelegate {
public final fun getReactInstanceManager ()Lcom/facebook/react/ReactInstanceManager;
public final fun getReactRootView ()Lcom/facebook/react/ReactRootView;
protected final fun isFabricEnabled ()Z
public final fun isFullyDrawnReportingEnabled ()Z
public final fun loadApp ()V
public final fun loadApp (Ljava/lang/String;)V
public final fun onActivityResult (IILandroid/content/Intent;Z)V
Expand All @@ -152,6 +154,7 @@ public class com/facebook/react/ReactDelegate {
public final fun onUserLeaveHint ()V
public final fun onWindowFocusChanged (Z)V
public final fun reload ()V
public final fun setFullyDrawnReportingEnabled (Z)V
public final fun setReactRootView (Lcom/facebook/react/ReactRootView;)V
public final fun setReactSurface (Lcom/facebook/react/interfaces/fabric/ReactSurface;)V
public final fun shouldShowDevMenuOrReload (ILandroid/view/KeyEvent;)Z
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ protected ReactRootView createRootView() {
}
};
}
mReactDelegate.setFullyDrawnReportingEnabled(isFullyDrawnReportingEnabled());
if (mainComponentName != null) {
LocalNetworkPermissionUtil.requestLocalNetworkAccessIfNeeded(
getPlainActivity(), () -> loadApp(mainComponentName));
Expand Down Expand Up @@ -322,4 +323,21 @@ protected boolean isFabricEnabled() {
protected boolean isWideColorGamutEnabled() {
return false;
}

/**
* Controls whether {@link Activity#reportFullyDrawn()} is automatically triggered once the
* initial content of the React surface has appeared, via the activity's {@code
* androidx.activity.FullyDrawnReporter}. Android uses this signal to bound the startup window for
* profile guided compilation (Android 12+) and to report time-to-fully-drawn in Android vitals.
*
* <p>Apps that consider themselves fully drawn only later (e.g. once their initial data has been
* rendered) can keep this enabled and additionally register their own reporter on the activity's
* {@code FullyDrawnReporter} before the content appears, or override this method to return false
* to opt out entirely.
*
* @return true if fully drawn reporting is enabled for this Activity, false otherwise.
*/
protected boolean isFullyDrawnReportingEnabled() {
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.view.KeyEvent
import androidx.activity.ComponentActivity
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.ReactMarker
import com.facebook.react.bridge.ReactMarkerConstants
import com.facebook.react.bridge.UiThreadUtil.runOnUiThread
import com.facebook.react.devsupport.DoubleTapReloadRecognizer
import com.facebook.react.devsupport.ReleaseDevSupportManager
Expand Down Expand Up @@ -51,6 +54,27 @@ public open class ReactDelegate {
*/
protected val isFabricEnabled: Boolean = true

/**
* Controls whether this delegate reports the host [Activity] as fully drawn once the initial
* content of the React surface has appeared.
*
* When enabled (the default) and the host [Activity] is a [ComponentActivity], a reporter is
* added to the activity's [androidx.activity.FullyDrawnReporter] when the surface starts loading
* and removed once the first view of the surface is mounted, which calls
* [Activity.reportFullyDrawn] after the next frame is drawn. Android uses this signal to bound
* the startup window for profile guided compilation (Android 12+) and to report
* time-to-fully-drawn in Android vitals.
*
* Apps that consider themselves fully drawn only later (e.g. once their initial data has been
* rendered) can keep this enabled and additionally register their own reporter on the activity's
* [androidx.activity.FullyDrawnReporter] before the content appears.
*
* Must be set before [loadApp] to take effect.
*/
public var isFullyDrawnReportingEnabled: Boolean = true

private var fullyDrawnMarkerListener: ReactMarker.MarkerListener? = null

/**
* Do not use this constructor as it's not accounting for New Architecture at all. You should use
* [ReactDelegate(Activity, ReactNativeHost, String, Bundle, boolean)] as it's the constructor
Expand Down Expand Up @@ -318,6 +342,9 @@ public open class ReactDelegate {
* @param appKey The ID of the app to load into the surface.
*/
public fun loadApp(appKey: String) {
if (isFullyDrawnReportingEnabled) {
reportFullyDrawnWhenContentAppears(appKey)
}
// With Bridgeless enabled, create and start the surface
if (ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture()) {
val reactHost = reactHost
Expand All @@ -340,6 +367,7 @@ public open class ReactDelegate {

/** Stop the React surface started with [ReactDelegate.loadApp]. */
public fun unloadApp() {
cancelFullyDrawnReporting()
if (ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture()) {
reactSurface?.stop()
reactSurface = null
Expand All @@ -351,6 +379,41 @@ public open class ReactDelegate {
}
}

private fun reportFullyDrawnWhenContentAppears(appKey: String) {
if (fullyDrawnMarkerListener != null) {
return
}
val fullyDrawnReporter = (activity as? ComponentActivity)?.fullyDrawnReporter ?: return
if (fullyDrawnReporter.isFullyDrawnReported) {
return
}
fullyDrawnReporter.addReporter()
val listener =
object : ReactMarker.MarkerListener {
override fun logMarker(name: ReactMarkerConstants, tag: String?, instanceKey: Int) {
if (name == ReactMarkerConstants.CONTENT_APPEARED && tag == appKey) {
// CONTENT_APPEARED is logged on the UI thread, so this cannot race with
// cancelFullyDrawnReporting or a second loadApp.
if (fullyDrawnMarkerListener !== this) {
return
}
fullyDrawnMarkerListener = null
ReactMarker.removeListener(this)
fullyDrawnReporter.removeReporter()
}
}
}
fullyDrawnMarkerListener = listener
ReactMarker.addListener(listener)
}

private fun cancelFullyDrawnReporting() {
val listener = fullyDrawnMarkerListener ?: return
fullyDrawnMarkerListener = null
ReactMarker.removeListener(listener)
(activity as? ComponentActivity)?.fullyDrawnReporter?.removeReporter()
}

public fun setReactSurface(reactSurface: ReactSurface?) {
this.reactSurface = reactSurface
}
Expand Down
Loading