Skip to content

Commit bef28be

Browse files
nbradburyclaude
andauthored
Fix Sentry JETPACK-ANDROID-1F1W: guard Gutenberg bridge callbacks against fragment detachment (#22974)
React Native bridge callbacks can run while the fragment detaches. Web view launchers now hop to the main thread and check isAdded() so the check cannot race with detachment; the synchronous media-options methods capture the activity/context once and use only the captured references. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent e246739 commit bef28be

1 file changed

Lines changed: 68 additions & 29 deletions

File tree

libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java

Lines changed: 68 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -624,34 +624,56 @@ private void initializeSavingProgressDialog() {
624624
}
625625
}
626626

627-
private GutenbergWebViewAuthorizationData getGutenbergWebViewAuthorizationData() {
628-
SavedInstanceDatabase db = SavedInstanceDatabase.Companion.getDatabase(getContext());
627+
@Nullable
628+
private GutenbergWebViewAuthorizationData getGutenbergWebViewAuthorizationData(@Nullable Context context) {
629+
// Callers pass a context they captured themselves: this can run on the React Native bridge
630+
// thread, where the fragment may detach at any point and Fragment.getContext() can become
631+
// null mid-call (JETPACK-ANDROID-1F1W)
632+
if (context == null) {
633+
return null;
634+
}
635+
SavedInstanceDatabase db = SavedInstanceDatabase.Companion.getDatabase(context);
629636
if (db != null) {
630637
return db.getParcel(ARG_GUTENBERG_WEB_VIEW_AUTH_DATA, GutenbergWebViewAuthorizationData.CREATOR);
631638
}
632639
return null;
633640
}
634641

635642
private void openGutenbergWebViewActivity(String content, String blockId, String blockName, String blockTitle) {
636-
GutenbergWebViewAuthorizationData gutenbergWebViewAuthData = getGutenbergWebViewAuthorizationData();
643+
// This is invoked from a React Native bridge callback that can fire while the fragment is
644+
// detaching. Hop to the main thread so the isAdded() check cannot race with detachment,
645+
// which also happens on the main thread (JETPACK-ANDROID-1F1W)
646+
new Handler(Looper.getMainLooper()).post(() -> {
647+
if (!isAdded()) {
648+
AppLog.e(T.EDITOR, "Failed to open Gutenberg web view because the fragment is not attached");
649+
return;
650+
}
637651

638-
// There is a chance that isJetpackSsoEnabled has changed on the server
639-
// so we need to make sure that we have fresh value of it.
640-
gutenbergWebViewAuthData.setJetpackSsoEnabled(mIsJetpackSsoEnabled);
652+
GutenbergWebViewAuthorizationData gutenbergWebViewAuthData =
653+
getGutenbergWebViewAuthorizationData(getContext());
654+
if (gutenbergWebViewAuthData == null) {
655+
AppLog.e(T.EDITOR, "Failed to open Gutenberg web view because the auth data is missing");
656+
return;
657+
}
641658

642-
Intent intent = new Intent(getActivity(), WPGutenbergWebViewActivity.class);
643-
intent.putExtra(WPGutenbergWebViewActivity.ARG_BLOCK_ID, blockId);
644-
intent.putExtra(WPGutenbergWebViewActivity.ARG_BLOCK_TITLE, blockTitle);
645-
intent.putExtra(WPGutenbergWebViewActivity.ARG_BLOCK_CONTENT, content);
646-
intent.putExtra(WPGutenbergWebViewActivity.ARG_GUTENBERG_WEB_VIEW_AUTH_DATA, gutenbergWebViewAuthData);
659+
// There is a chance that isJetpackSsoEnabled has changed on the server
660+
// so we need to make sure that we have fresh value of it.
661+
gutenbergWebViewAuthData.setJetpackSsoEnabled(mIsJetpackSsoEnabled);
647662

648-
startActivityForResult(intent, UNSUPPORTED_BLOCK_REQUEST_CODE);
663+
Intent intent = new Intent(getActivity(), WPGutenbergWebViewActivity.class);
664+
intent.putExtra(WPGutenbergWebViewActivity.ARG_BLOCK_ID, blockId);
665+
intent.putExtra(WPGutenbergWebViewActivity.ARG_BLOCK_TITLE, blockTitle);
666+
intent.putExtra(WPGutenbergWebViewActivity.ARG_BLOCK_CONTENT, content);
667+
intent.putExtra(WPGutenbergWebViewActivity.ARG_GUTENBERG_WEB_VIEW_AUTH_DATA, gutenbergWebViewAuthData);
649668

650-
HashMap<String, String> properties = new HashMap<>();
651-
properties.put("block", blockName);
652-
mEditorFragmentListener.onTrackableEvent(
653-
TrackableEvent.EDITOR_GUTENBERG_UNSUPPORTED_BLOCK_WEBVIEW_SHOWN,
654-
properties);
669+
startActivityForResult(intent, UNSUPPORTED_BLOCK_REQUEST_CODE);
670+
671+
HashMap<String, String> properties = new HashMap<>();
672+
properties.put("block", blockName);
673+
mEditorFragmentListener.onTrackableEvent(
674+
TrackableEvent.EDITOR_GUTENBERG_UNSUPPORTED_BLOCK_WEBVIEW_SHOWN,
675+
properties);
676+
});
655677
}
656678

657679
private void trackWebViewClosed(String action) {
@@ -663,12 +685,19 @@ private void trackWebViewClosed(String action) {
663685
}
664686

665687
private void openGutenbergEmbedWebViewActivity(String html, String title) {
666-
Activity activity = getActivity();
688+
// Same bridge-thread detachment hazard as openGutenbergWebViewActivity (JETPACK-ANDROID-1F1W)
689+
new Handler(Looper.getMainLooper()).post(() -> {
690+
Activity activity = getActivity();
691+
if (activity == null) {
692+
AppLog.e(T.EDITOR, "Failed to open Gutenberg embed web view because the fragment is not attached");
693+
return;
694+
}
667695

668-
Intent intent = new Intent(activity, GutenbergEmbedWebViewActivity.class);
669-
intent.putExtra(GutenbergEmbedWebViewActivity.ARG_CONTENT, html);
670-
intent.putExtra(GutenbergEmbedWebViewActivity.ARG_TITLE, title);
671-
activity.startActivityForResult(intent, EMBED_FULLSCREEN_PREVIEW_CODE);
696+
Intent intent = new Intent(activity, GutenbergEmbedWebViewActivity.class);
697+
intent.putExtra(GutenbergEmbedWebViewActivity.ARG_CONTENT, html);
698+
intent.putExtra(GutenbergEmbedWebViewActivity.ARG_TITLE, title);
699+
activity.startActivityForResult(intent, EMBED_FULLSCREEN_PREVIEW_CODE);
700+
});
672701
}
673702

674703
@Override
@@ -699,17 +728,25 @@ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent d
699728
private ArrayList<MediaOption> initOtherMediaImageOptions() {
700729
ArrayList<MediaOption> otherMediaOptions = new ArrayList<>();
701730

731+
// This runs on the React Native bridge thread and must return synchronously, so capture the
732+
// activity and context once and use only the captured references below — Fragment.getString()
733+
// and friends can start throwing mid-call if the fragment detaches (JETPACK-ANDROID-1F1W)
702734
Bundle arguments = getArguments();
703735
FragmentActivity activity = getActivity();
704736
final Context context = getContext();
705737
if (activity == null || context == null || arguments == null) {
706738
AppLog.e(T.EDITOR,
707-
"Failed to initialize other media options because the activity or getArguments() is null");
739+
"Failed to initialize other media options because the activity, context or arguments are null");
708740
return otherMediaOptions;
709741
}
710742

711743
boolean jetpackFeaturesEnabled = arguments.getBoolean(ARG_JETPACK_FEATURES_ENABLED);
712-
GutenbergWebViewAuthorizationData gutenbergWebViewAuthorizationData = getGutenbergWebViewAuthorizationData();
744+
GutenbergWebViewAuthorizationData gutenbergWebViewAuthorizationData =
745+
getGutenbergWebViewAuthorizationData(context);
746+
if (gutenbergWebViewAuthorizationData == null) {
747+
AppLog.e(T.EDITOR, "Failed to initialize other media options because the auth data is missing");
748+
return otherMediaOptions;
749+
}
713750

714751
boolean supportStockPhotos = gutenbergWebViewAuthorizationData.isSiteUsingWPComRestAPI()
715752
&& jetpackFeaturesEnabled;
@@ -719,7 +756,7 @@ private ArrayList<MediaOption> initOtherMediaImageOptions() {
719756
int stockMediaResourceId =
720757
context.getResources().getIdentifier("photo_picker_stock_media", "string", packageName);
721758

722-
otherMediaOptions.add(new MediaOption(MEDIA_SOURCE_STOCK_MEDIA, getString(stockMediaResourceId)));
759+
otherMediaOptions.add(new MediaOption(MEDIA_SOURCE_STOCK_MEDIA, context.getString(stockMediaResourceId)));
723760
}
724761

725762
return otherMediaOptions;
@@ -736,19 +773,21 @@ private ArrayList<MediaOption> initOtherMediaAudioFileOptions() {
736773
private ArrayList<MediaOption> initOtherMediaFileOptions(String mediaOptionId) {
737774
ArrayList<MediaOption> otherMediaOptions = new ArrayList<>();
738775

776+
// Capture and use only local references, as in initOtherMediaImageOptions (JETPACK-ANDROID-1F1W)
739777
FragmentActivity activity = getActivity();
740-
if (activity == null) {
778+
final Context context = getContext();
779+
if (activity == null || context == null) {
741780
AppLog.e(T.EDITOR,
742-
"Failed to initialize other media options because the activity is null");
781+
"Failed to initialize other media options because the activity or context is null");
743782
return otherMediaOptions;
744783
}
745784

746785
String packageName = activity.getApplication().getPackageName();
747786

748787
int chooseFileResourceId =
749-
getResources().getIdentifier("photo_picker_choose_file", "string", packageName);
788+
context.getResources().getIdentifier("photo_picker_choose_file", "string", packageName);
750789

751-
otherMediaOptions.add(new MediaOption(mediaOptionId, getString(chooseFileResourceId)));
790+
otherMediaOptions.add(new MediaOption(mediaOptionId, context.getString(chooseFileResourceId)));
752791

753792
return otherMediaOptions;
754793
}

0 commit comments

Comments
 (0)