@@ -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
@@ -420,7 +422,7 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
420422 // triggers ClickableSpan onClick() events
421423 movementMethod = EnhancedMovementMethod
422424
423- setupBackspaceAndEnterDetection ()
425+ setupKeyListenersAndInputFilters ()
424426
425427 // disable auto suggestions/correct for older devices
426428 if (Build .VERSION .SDK_INT < Build .VERSION_CODES .LOLLIPOP ) {
@@ -440,12 +442,52 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
440442 // Setup the keyListener(s) for Backspace and Enter key.
441443 // Backspace: If listener does return false we remove the style here
442444 // Enter: Ask the listener if we need to insert or not the char
443- private fun setupBackspaceAndEnterDetection () {
445+ private fun setupKeyListenersAndInputFilters () {
444446 // hardware keyboard
445447 setOnKeyListener { _, _, event ->
446448 handleBackspaceAndEnter(event)
447449 }
448450
451+ // This InputFilter created only for the purpose of avoiding crash described here:
452+ // https://android-review.googlesource.com/c/platform/frameworks/base/+/634929
453+ // https://github.com/wordpress-mobile/AztecEditor-Android/issues/729
454+ // the rationale behind this workaround is that the specific crash happens only when adding/deleting text right
455+ // before an AztecImageSpan, so we detect the specific case and re-create the contents only when that happens.
456+ // This is indeed tackling the symptom rather than the actual problem, and should be removed once the real
457+ // problem is fixed at the Android OS level as described in the following url
458+ // https://android-review.googlesource.com/c/platform/frameworks/base/+/634929
459+ val dynamicLayoutCrashPreventer = InputFilter { source, start, end, dest, dstart, dend ->
460+ var temp : CharSequence? = null
461+ if (! bypassCrashPreventerInputFilter && dstart == dend && dest.length > dend+ 1 ) {
462+ // dstart == dend means this is an insertion
463+ // if there are any images right after the destination position, hack the text
464+ val spans = dest.getSpans(dstart, dend+ 1 , AztecImageSpan ::class .java)
465+ if (spans.isNotEmpty()) {
466+ // prevent this filter from running twice when `text.insert()` gets called a few lines below
467+ disableCrashPreventerInputFilter()
468+ // disable MediaDeleted listener before operating on content
469+ disableMediaDeletedListener()
470+
471+ // take the source (that is, what is being inserted), and append the Image to it. We will delete
472+ // the original Image later so to not have a duplicate.
473+ // use Spannable to copy / keep the current spans
474+ temp = SpannableStringBuilder (source).append(dest.subSequence(dend, dend+ 1 ))
475+
476+ // delete the original AztecImageSpan
477+ text.delete(dend, dend+ 1 )
478+ // now insert both the new insertion _and_ the original AztecImageSpan
479+ text.insert(dend, temp)
480+ temp = " " // discard the original source parameter as an ouput from this InputFilter
481+
482+ // re-enable MediaDeleted listener
483+ enableMediaDeletedListener()
484+ // re-enable this very filter
485+ enableCrashPreventerInputFilter()
486+ }
487+ }
488+ temp
489+ }
490+
449491 val emptyEditTextBackspaceDetector = InputFilter { source, start, end, dest, dstart, dend ->
450492 if (selectionStart == 0 && selectionEnd == 0
451493 && end == 0 && start == 0
@@ -462,7 +504,12 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
462504 source
463505 }
464506
465- filters = arrayOf(emptyEditTextBackspaceDetector)
507+ if (Build .VERSION .SDK_INT == Build .VERSION_CODES .O || Build .VERSION .SDK_INT == Build .VERSION_CODES .O_MR1 ) {
508+ // dynamicLayoutCrashPreventer needs to be first in array as these are going to be chained when processed
509+ filters = arrayOf(dynamicLayoutCrashPreventer, emptyEditTextBackspaceDetector)
510+ } else {
511+ filters = arrayOf(emptyEditTextBackspaceDetector)
512+ }
466513 }
467514
468515 private fun handleBackspaceAndEnter (event : KeyEvent ): Boolean {
@@ -1303,6 +1350,26 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
13031350 bypassObservationQueue = false
13041351 }
13051352
1353+ fun disableCrashPreventerInputFilter () {
1354+ bypassCrashPreventerInputFilter = true
1355+ }
1356+
1357+ fun enableCrashPreventerInputFilter () {
1358+ bypassCrashPreventerInputFilter = false
1359+ }
1360+
1361+ fun disableMediaDeletedListener () {
1362+ bypassMediaDeletedListener = true
1363+ }
1364+
1365+ fun enableMediaDeletedListener () {
1366+ bypassMediaDeletedListener = false
1367+ }
1368+
1369+ fun isMediaDeletedListenerDisabled (): Boolean {
1370+ return bypassMediaDeletedListener
1371+ }
1372+
13061373 fun isTextChangedListenerDisabled (): Boolean {
13071374 return consumeEditEvent
13081375 }
0 commit comments