Skip to content

Commit d633ab4

Browse files
Fix TextInput crash when clipboard read access is denied on Android
ClipboardManager throws a SecurityException when the READ_CLIPBOARD app op is set to MODE_ERRORED for the app, which some OEM clipboard privacy settings do. TextView reads the clipboard from its floating text menu, keyboard shortcuts, accessibility info and actions, and when pasting, so a tap, a long-press, Ctrl+V or an accessibility service focusing a TextInput crashes the app. Guard those entry points in ReactEditText and treat a denial as an unavailable clipboard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 6e22123 commit d633ab4

1 file changed

Lines changed: 68 additions & 3 deletions

File tree

  • packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt‎

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import android.text.method.KeyListener
3434
import android.text.method.QwertyKeyListener
3535
import android.util.TypedValue
3636
import android.view.ActionMode
37+
import android.view.ContextMenu
3738
import android.view.DragEvent
3839
import android.view.Gravity
3940
import android.view.KeyEvent
@@ -424,9 +425,73 @@ public open class ReactEditText public constructor(context: Context) : AppCompat
424425
* React Native replaces copy (as rich text) with copy as plain text.
425426
*/
426427
override fun onTextContextMenuItem(id: Int): Boolean =
427-
super.onTextContextMenuItem(
428-
if (id == android.R.id.paste) android.R.id.pasteAsPlainText else id,
429-
)
428+
withClipboardAccessGuard(fallback = false) {
429+
super.onTextContextMenuItem(
430+
if (id == android.R.id.paste) android.R.id.pasteAsPlainText else id,
431+
)
432+
}
433+
434+
override fun startActionMode(callback: ActionMode.Callback, type: Int): ActionMode? =
435+
super.startActionMode(ClipboardSafeActionModeCallback(callback), type)
436+
437+
override fun onKeyShortcut(keyCode: Int, event: KeyEvent): Boolean =
438+
withClipboardAccessGuard(fallback = false) { super.onKeyShortcut(keyCode, event) }
439+
440+
override fun onCreateContextMenu(menu: ContextMenu) {
441+
withClipboardAccessGuard(fallback = Unit) { super.onCreateContextMenu(menu) }
442+
}
443+
444+
override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
445+
withClipboardAccessGuard(fallback = Unit) { super.onInitializeAccessibilityNodeInfo(info) }
446+
}
447+
448+
override fun performAccessibilityAction(action: Int, arguments: Bundle?): Boolean =
449+
withClipboardAccessGuard(fallback = false) {
450+
super.performAccessibilityAction(action, arguments)
451+
}
452+
453+
/*
454+
* ClipboardManager throws a SecurityException when the READ_CLIPBOARD app op is set to
455+
* MODE_ERRORED for the app, which some OEM clipboard privacy settings do. TextView reads the
456+
* clipboard from its text menus, keyboard shortcuts, accessibility info and actions, and when
457+
* pasting, so treat a denial as an unavailable clipboard instead of crashing.
458+
*/
459+
private inline fun <T> withClipboardAccessGuard(fallback: T, block: () -> T): T =
460+
try {
461+
block()
462+
} catch (e: SecurityException) {
463+
FLog.w(TAG, "Clipboard access denied", e)
464+
fallback
465+
}
466+
467+
/*
468+
* Editor builds the floating text menu in onCreateActionMode, often from a posted runnable
469+
* (smart selection), so no ReactEditText method is on the stack when TextView.canPaste()
470+
* throws. Returning false from onCreate/onPrepare cancels the menu instead of crashing.
471+
*/
472+
private inner class ClipboardSafeActionModeCallback(private val delegate: ActionMode.Callback) :
473+
ActionMode.Callback2() {
474+
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean =
475+
withClipboardAccessGuard(fallback = false) { delegate.onCreateActionMode(mode, menu) }
476+
477+
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean =
478+
withClipboardAccessGuard(fallback = false) { delegate.onPrepareActionMode(mode, menu) }
479+
480+
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean =
481+
withClipboardAccessGuard(fallback = false) { delegate.onActionItemClicked(mode, item) }
482+
483+
override fun onDestroyActionMode(mode: ActionMode) {
484+
delegate.onDestroyActionMode(mode)
485+
}
486+
487+
override fun onGetContentRect(mode: ActionMode, view: View, outRect: Rect) {
488+
if (delegate is ActionMode.Callback2) {
489+
delegate.onGetContentRect(mode, view, outRect)
490+
} else {
491+
super.onGetContentRect(mode, view, outRect)
492+
}
493+
}
494+
}
430495

431496
internal fun clearFocusAndMaybeRefocus() {
432497
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P || !isInTouchMode) {

0 commit comments

Comments
 (0)