jump-sdk / jetpack_compose_country_code_picker_emoji

Jetpack Compose Country Code Picker with Emoji Flags
https://jitpack.io/#jump-sdk/jetpack_compose_country_code_picker_emoji
Apache License 2.0
48 stars 17 forks source link

Visual transformation causing the app to crash on compose v1.7.2 #65

Open rungxanh1995 opened 1 week ago

rungxanh1995 commented 1 week ago

Describe the bug Recently, entering phone number to the text field is crashing the app.

For example: I want to enter a Canadian phone number +14165550000. The code will crash as soon as I've entered "4165550" into the text field.

Important note: The code runs well on its own, without any extra dependencies or dependency version upgrades. Then I integrated the library to my project, updated dependency versions and made some necessary edits, it's when the issue arise. The visual transformation logic no longer works in a reliable manner, crashing the app as offset mapping is now flawed.

Refer to my Github fork commits to see the steps I made to edit visual transformation logic to ensure the app doesn't crash.

To Reproduce Steps to reproduce the behavior:

  1. Tap the country flag to pick Canada
  2. Tap the text field to start entering your phone number
  3. Type 4165550
  4. Type the next number and observe the app crashes

Expected behavior The app shouldn't have crashed, but instead handle visually transforming the input as before.

Smartphone (please complete the following information):

Additional context Refer to the stack trace error below

FATAL EXCEPTION: main (Ask Gemini)
Process: com.togitech.togii, PID: 15552
java.lang.IllegalStateException: OffsetMapping.transformedToOriginal returned invalid mapping: 0 -> -1 is not in range of original text [0, 8]
    at androidx.compose.foundation.text.ValidatingOffsetMappingKt.validateTransformedToOriginal(ValidatingOffsetMapping.kt:116)
    at androidx.compose.foundation.text.ValidatingOffsetMappingKt.throwIfNotValidTransform(ValidatingOffsetMapping.kt:73)
    at androidx.compose.foundation.text.ValidatingOffsetMappingKt.throwIfNotValidTransform$default(ValidatingOffsetMapping.kt:60)
    at androidx.compose.foundation.text.ValidatingOffsetMappingKt.filterWithValidation(ValidatingOffsetMapping.kt:35)
    at androidx.compose.foundation.text.CoreTextFieldKt.CoreTextField(CoreTextField.kt:246)
    at androidx.compose.foundation.text.BasicTextFieldKt.BasicTextField(BasicTextField.kt:765)
    at androidx.compose.material.OutlinedTextFieldKt.OutlinedTextField(OutlinedTextField.kt:379)
    at com.togitech.ccp.component.TogiCountryCodePickerKt.TogiCountryCodePicker(TogiCountryCodePicker.kt:173)
    at com.togitech.togii.MainActivityKt.CountryCodePick(MainActivity.kt:77)
    at com.togitech.togii.MainActivityKt$CountryCodePick$2.invoke(Unknown Source:8)
    at com.togitech.togii.MainActivityKt$CountryCodePick$2.invoke(Unknown Source:10)
    at androidx.compose.runtime.RecomposeScopeImpl.compose(RecomposeScopeImpl.kt:192)
    at androidx.compose.runtime.ComposerImpl.recomposeToGroupEnd(Composer.kt:2825)
    at androidx.compose.runtime.ComposerImpl.skipCurrentGroup(Composer.kt:3116)
    at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:3607)
    at androidx.compose.runtime.ComposerImpl.recompose$runtime_release(Composer.kt:3552)
    at androidx.compose.runtime.CompositionImpl.recompose(Composition.kt:948)
    at androidx.compose.runtime.Recomposer.performRecompose(Recomposer.kt:1206)
    at androidx.compose.runtime.Recomposer.access$performRecompose(Recomposer.kt:132)
    at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$1.invoke(Recomposer.kt:616)
    at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$1.invoke(Recomposer.kt:585)
    at androidx.compose.ui.platform.AndroidUiFrameClock$withFrameNanos$2$callback$1.doFrame(AndroidUiFrameClock.android.kt:41)
    at androidx.compose.ui.platform.AndroidUiDispatcher.performFrameDispatch(AndroidUiDispatcher.android.kt:109)
    at androidx.compose.ui.platform.AndroidUiDispatcher.access$performFrameDispatch(AndroidUiDispatcher.android.kt:41)
    at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.doFrame(AndroidUiDispatcher.android.kt:69)
    at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1337)
    at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1348)
    at android.view.Choreographer.doCallbacks(Choreographer.java:952)
    at android.view.Choreographer.doFrame(Choreographer.java:878)
    at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1322)
    at android.os.Handler.handleCallback(Handler.java:958)
    at android.os.Handler.dispatchMessage(Handler.java:99)
    at android.os.Looper.loopOnce(Looper.java:205)
    at android.os.Looper.loop(Looper.java:294)
    at android.app.ActivityThread.main(ActivityThread.java:8177)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
    Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [androidx.compose.runtime.PausableMonotonicFrameClock@53d2e8, androidx.compose.ui.platform.MotionDurationScaleImpl@563ad01, StandaloneCoroutine{Cancelling}@9ed27a6, AndroidUiDispatcher@c72a5e7]
rungxanh1995 commented 1 week ago

At the moment, I'm using 0 as the fallback index to reformat the input text to not crash the app, as follow:

// PhoneNumberTransformation.kt
@Suppress("AvoidMutableCollections", "AvoidVarsExceptWithDelegate")
    private fun reformat(s: CharSequence, cursor: Int): Transformation {
        phoneNumberFormatter.clear()

        val curIndex = cursor - 1
        var formatted: String? = null
        var lastNonSeparator = 0.toChar()
        var hasCursor = false

        s.forEachIndexed { index, char ->
            if (PhoneNumberUtils.isNonSeparator(char)) {
                if (lastNonSeparator.code != 0) {
                    formatted = getFormattedNumber(lastNonSeparator, hasCursor)
                    hasCursor = false
                }
                lastNonSeparator = char
            }
            if (index == curIndex) {
                hasCursor = true
            }
        }

        if (lastNonSeparator.code != 0) {
            formatted = getFormattedNumber(lastNonSeparator, hasCursor)
        }
        val originalToTransformed = mutableListOf<Int>()
        val transformedToOriginal = mutableListOf<Int>()
        var specialCharsCount = 0
        formatted?.forEachIndexed { index, char ->
            if (!PhoneNumberUtils.isNonSeparator(char)) {
                specialCharsCount++
            } else {
                originalToTransformed.add(index)
            }

            // transformedToOriginal.add(index - specialCharsCount) // Disabled this line
            transformedToOriginal.add(maxOf(index - specialCharsCount, 0)) // -> Changed to this line
        }
        originalToTransformed.add(originalToTransformed.maxOrNull()?.plus(1) ?: 0)
        transformedToOriginal.add(transformedToOriginal.maxOrNull()?.plus(1) ?: 0)

        return Transformation(formatted, originalToTransformed, transformedToOriginal)
    }