@@ -224,6 +224,8 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
224224 private var consumeSelectionChangedEvent: Boolean = false
225225 private var isInlineTextHandlerEnabled: Boolean = true
226226 private var bypassObservationQueue: Boolean = false
227+ private var bypassMediaDeletedListener: Boolean = false
228+ private var bypassCrashPreventerInputFilter: Boolean = false
227229
228230 var initialEditorContentParsedSHA256: ByteArray = ByteArray (0 )
229231
@@ -322,6 +324,10 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
322324 fun onBackspaceKey () : Boolean
323325 }
324326
327+ interface OnLinkTappedListener {
328+ fun onLinkTapped (widget : View , url : String )
329+ }
330+
325331 constructor (context: Context ) : super (context) {
326332 init (null )
327333 }
@@ -420,7 +426,7 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
420426 // triggers ClickableSpan onClick() events
421427 movementMethod = EnhancedMovementMethod
422428
423- setupBackspaceAndEnterDetection ()
429+ setupKeyListenersAndInputFilters ()
424430
425431 // disable auto suggestions/correct for older devices
426432 if (Build .VERSION .SDK_INT < Build .VERSION_CODES .LOLLIPOP ) {
@@ -440,12 +446,52 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
440446 // Setup the keyListener(s) for Backspace and Enter key.
441447 // Backspace: If listener does return false we remove the style here
442448 // Enter: Ask the listener if we need to insert or not the char
443- private fun setupBackspaceAndEnterDetection () {
449+ private fun setupKeyListenersAndInputFilters () {
444450 // hardware keyboard
445451 setOnKeyListener { _, _, event ->
446452 handleBackspaceAndEnter(event)
447453 }
448454
455+ // This InputFilter created only for the purpose of avoiding crash described here:
456+ // https://android-review.googlesource.com/c/platform/frameworks/base/+/634929
457+ // https://github.com/wordpress-mobile/AztecEditor-Android/issues/729
458+ // the rationale behind this workaround is that the specific crash happens only when adding/deleting text right
459+ // before an AztecImageSpan, so we detect the specific case and re-create the contents only when that happens.
460+ // This is indeed tackling the symptom rather than the actual problem, and should be removed once the real
461+ // problem is fixed at the Android OS level as described in the following url
462+ // https://android-review.googlesource.com/c/platform/frameworks/base/+/634929
463+ val dynamicLayoutCrashPreventer = InputFilter { source, start, end, dest, dstart, dend ->
464+ var temp : CharSequence? = null
465+ if (! bypassCrashPreventerInputFilter && dstart == dend && dest.length > dend+ 1 ) {
466+ // dstart == dend means this is an insertion
467+ // if there are any images right after the destination position, hack the text
468+ val spans = dest.getSpans(dstart, dend+ 1 , AztecImageSpan ::class .java)
469+ if (spans.isNotEmpty()) {
470+ // prevent this filter from running twice when `text.insert()` gets called a few lines below
471+ disableCrashPreventerInputFilter()
472+ // disable MediaDeleted listener before operating on content
473+ disableMediaDeletedListener()
474+
475+ // take the source (that is, what is being inserted), and append the Image to it. We will delete
476+ // the original Image later so to not have a duplicate.
477+ // use Spannable to copy / keep the current spans
478+ temp = SpannableStringBuilder (source).append(dest.subSequence(dend, dend+ 1 ))
479+
480+ // delete the original AztecImageSpan
481+ text.delete(dend, dend+ 1 )
482+ // now insert both the new insertion _and_ the original AztecImageSpan
483+ text.insert(dend, temp)
484+ temp = " " // discard the original source parameter as an ouput from this InputFilter
485+
486+ // re-enable MediaDeleted listener
487+ enableMediaDeletedListener()
488+ // re-enable this very filter
489+ enableCrashPreventerInputFilter()
490+ }
491+ }
492+ temp
493+ }
494+
449495 val emptyEditTextBackspaceDetector = InputFilter { source, start, end, dest, dstart, dend ->
450496 if (selectionStart == 0 && selectionEnd == 0
451497 && end == 0 && start == 0
@@ -462,7 +508,12 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
462508 source
463509 }
464510
465- filters = arrayOf(emptyEditTextBackspaceDetector)
511+ if (Build .VERSION .SDK_INT == Build .VERSION_CODES .O || Build .VERSION .SDK_INT == Build .VERSION_CODES .O_MR1 ) {
512+ // dynamicLayoutCrashPreventer needs to be first in array as these are going to be chained when processed
513+ filters = arrayOf(dynamicLayoutCrashPreventer, emptyEditTextBackspaceDetector)
514+ } else {
515+ filters = arrayOf(emptyEditTextBackspaceDetector)
516+ }
466517 }
467518
468519 private fun handleBackspaceAndEnter (event : KeyEvent ): Boolean {
@@ -786,6 +837,14 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
786837 this .onVideoInfoRequestedListener = listener
787838 }
788839
840+ fun setOnLinkTappedListener (listener : OnLinkTappedListener ) {
841+ EnhancedMovementMethod .linkTappedListener = listener
842+ }
843+
844+ fun setLinkTapEnabled (isLinkTapEnabled : Boolean ) {
845+ EnhancedMovementMethod .isLinkTapEnabled = isLinkTapEnabled
846+ }
847+
789848 override fun onKeyPreIme (keyCode : Int , event : KeyEvent ): Boolean {
790849 if (event.keyCode == KeyEvent .KEYCODE_BACK && event.action == KeyEvent .ACTION_UP ) {
791850 onImeBackListener?.onImeBack()
@@ -1303,6 +1362,26 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
13031362 bypassObservationQueue = false
13041363 }
13051364
1365+ fun disableCrashPreventerInputFilter () {
1366+ bypassCrashPreventerInputFilter = true
1367+ }
1368+
1369+ fun enableCrashPreventerInputFilter () {
1370+ bypassCrashPreventerInputFilter = false
1371+ }
1372+
1373+ fun disableMediaDeletedListener () {
1374+ bypassMediaDeletedListener = true
1375+ }
1376+
1377+ fun enableMediaDeletedListener () {
1378+ bypassMediaDeletedListener = false
1379+ }
1380+
1381+ fun isMediaDeletedListenerDisabled (): Boolean {
1382+ return bypassMediaDeletedListener
1383+ }
1384+
13061385 fun isTextChangedListenerDisabled (): Boolean {
13071386 return consumeEditEvent
13081387 }
@@ -1778,4 +1857,5 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
17781857 override fun dispatchHoverEvent (event : MotionEvent ): Boolean {
17791858 return if (accessibilityDelegate.onHoverEvent(event)) true else super .dispatchHoverEvent(event)
17801859 }
1860+
17811861}
0 commit comments