NullPointerException on Meizu devices in Editor.updateCursorPositionMz

AndroidTextviewMeizu

Android Problem Overview


Lately, there have been crashes on my Android app, on Meizu devices only (M5c, M5s, M5 Note). Android version: 6.0.

Here is the full stack trace:

Fatal Exception: java.lang.NullPointerException: Attempt to invoke virtual method 'int android.text.Layout.getLineForOffset(int)' on a null object reference
   at android.widget.Editor.updateCursorPositionMz(Editor.java:6964)
   at android.widget.Editor.updateCursorsPositions(Editor.java:1760)
   at android.widget.TextView.getUpdatedHighlightPath(TextView.java:5689)
   at android.widget.TextView.onDraw(TextView.java:5882)
   at android.view.View.draw(View.java:16539)
   at android.view.View.updateDisplayListIfDirty(View.java:15492)
   at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
   at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
   at android.view.View.updateDisplayListIfDirty(View.java:15443)
   at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
   at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
   at android.view.View.updateDisplayListIfDirty(View.java:15443)
   at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
   at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
   at android.view.View.updateDisplayListIfDirty(View.java:15443)
   at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
   at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
   at android.view.View.updateDisplayListIfDirty(View.java:15443)
   at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
   at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
   at android.view.View.updateDisplayListIfDirty(View.java:15443)
   at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
   at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
   at android.view.View.updateDisplayListIfDirty(View.java:15443)
   at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
   at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
   at android.view.View.updateDisplayListIfDirty(View.java:15443)
   at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
   at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
   at android.view.View.updateDisplayListIfDirty(View.java:15443)
   at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
   at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
   at android.view.View.updateDisplayListIfDirty(View.java:15443)
   at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
   at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
   at android.view.View.updateDisplayListIfDirty(View.java:15443)
   at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
   at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
   at android.view.View.updateDisplayListIfDirty(View.java:15443)
   at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
   at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
   at android.view.View.updateDisplayListIfDirty(View.java:15443)
   at android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java:286)
   at android.view.ThreadedRenderer.updateRootDisplayList(ThreadedRenderer.java:292)
   at android.view.ThreadedRenderer.draw(ThreadedRenderer.java:327)
   at android.view.ViewRootImpl.draw(ViewRootImpl.java:3051)
   at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:2855)
   at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2464)
   at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1337)
   at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6819)
   at android.view.Choreographer$CallbackRecord.run(Choreographer.java:894)
   at android.view.Choreographer.doCallbacks(Choreographer.java:696)
   at android.view.Choreographer.doFrame(Choreographer.java:631)
   at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:880)
   at android.os.Handler.handleCallback(Handler.java:815)
   at android.os.Handler.dispatchMessage(Handler.java:104)
   at android.os.Looper.loop(Looper.java:207)
   at android.app.ActivityThread.main(ActivityThread.java:5969)
   at java.lang.reflect.Method.invoke(Method.java)
   at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:830)
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:720)

There is no direct relation to my code (even in other threads' stracktraces). I only know that it happens everytime in a Fragment in which there are TextViews. It might be happening when a TextView is gaining focus but I have no way of being sure. Of course I cannot reproduce the bug, unless I buy a Meizu.

Also, since the top method is called updateCursorPositionMz, it looks to me like this may be an internal issue in Meizu's FlymeOS ("Mz" = "Meizu"?).

Has anyone already had this issue, knows the cause and how to fix it?

Thanks.

Android Solutions


Solution 1 - Android

Update (Aug. 8, 2019)

As @andreas-wenger, @waseefakhtar and @vadim-kotov mentioned, the fix is now included from com.google.android.material:material:1.1.0-alpha08 onwards.

Old answer

Finally I had the chance to put my hands on a Meizu. As I thought, the crash occurs every time the user clicks on a field to get the focus.

In my case, I had some android.support.design.widget.TextInputEditText inside TextInputLayouts. Simply replacing these TextInputEditTexts with AppCompatEditTexts fixed the problem, like so:

<android.support.design.widget.TextInputLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="...">

    <android.support.v7.widget.AppCompatEditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
        
</android.support.design.widget.TextInputLayout>

The behavior remains the same (since TextInputEditText extends AppCompatEditText). I still haven't found the root cause of the problem though.

Solution 2 - Android

This was just fixed in the material components for Android lib, see: https://github.com/material-components/material-components-android/pull/358

Solution 3 - Android

In my case, I verified that using AppCompatEditText instead of TextInputEditText indeed prevented crashes, but we couldn't use this solution. We're using an sdk with views which extend TextInputEditText, so switching to AppCompatEditText would require copying/modifying quite a bit of the sdk code into our project.

I tried setting the hint on both the TextInputEditText and TextInputLayout, but I ended up seeing a double hint (like blurry text, and I'm sure I didn't drink too much).

I took a look at the GitHub issue linked to by @Andrew: https://github.com/android-in-china/Compatibility/issues/11

In that issue, they explain that the root cause is a problem on Meizu when TextInputEditText.getHint() is different from TextInputEditText.mHint.

When a TextInputEditText is inside a TextInputLayout, and the hint is specified in xml on the TextInputEditText, the support library basically "moves" the hint to the containing TextInputLayout: it sets it on the container and then sets it to null on the edit text.

This source that does this is in TextInputLayout.setEditText():

    // If we do not have a valid hint, try and retrieve it from the EditText, if enabled
    if (hintEnabled) {
      if (TextUtils.isEmpty(hint)) {
        // Save the hint so it can be restored on dispatchProvideAutofillStructure();
        originalHint = this.editText.getHint();
        setHint(originalHint);
        // Clear the EditText's hint as we will display it ourselves
        this.editText.setHint(null);
      }

Then when you call TextInputEditText.getHint(), it will return the hint of the container.

This inconsistency between the getHint() (the hint value) and mHint (null) seems to pose a problem for Meizu devices

I found another way to avoid this issue.

On Meizu devices, I:

  1. programmatically reset the TextInputEditText's hint back to what it was set to originally from xml (by calling its overridden getHint() which returns the container's hint).

  2. set the TextInputEditText's hint color to transparent, to avoid the double/blurry hint effect:

    private void hackFixHintsForMeizu(TextInputEditText... editTexts) { String manufacturer = Build.MANUFACTURER.toUpperCase(Locale.US); if (manufacturer.contains("MEIZU")) { for (TextInputEditText editText : editTexts) { editText.setHintTextColor(Color.TRANSPARENT); editText.setHint(editText.getHint()); } } }

Solution 4 - Android

I based my solution on the FixedTextInputEditText as mentioned in https://github.com/android-in-china/Compatibility/issues/11#issuecomment-427560370.

First off all I created a fixed TextInputEditText instance:

public class MeizuTextInputEditText extends TextInputEditText {
    public MeizuTextInputEditText(Context context) {
        super(context);
    }

    public MeizuTextInputEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MeizuTextInputEditText(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public CharSequence getHint() {
        try {
            return getMeizuHintHack();
        } catch (Exception e) {
            return super.getHint();
        }
    }

    private CharSequence getMeizuHintHack() throws NoSuchFieldException, IllegalAccessException {
        Field textView = TextView.class.getDeclaredField("mHint");
        textView.setAccessible(true);
        return (CharSequence) textView.get(this);
    }
}

But then I would have to replace all of my TextInputEditText usages with MeizuTextInputEditText which is not something you can easily do on a bigger codebase. Also when creating future views you always need to consider using the MeizuTextInputEditText instead of the 'broken' one. Forgetting about it would easily introduce production issues again.

So the final fix consists of the custom view class and together with the ViewPump library (https://github.com/InflationX/ViewPump) we can easily do that. Just as explained in the docs you need to register a custom intercepter that looks like this one:

public class TextInputEditTextInterceptor implements Interceptor {
    @Override
    public InflateResult intercept(Chain chain) {
        InflateRequest request = chain.request();
        View view = inflateView(request.name(), request.context(), request.attrs());

        if (view != null) {
            return InflateResult.builder()
                    .view(view)
                    .name(view.getClass().getName())
                    .context(request.context())
                    .attrs(request.attrs())
                    .build();
        } else {
            return chain.proceed(request);
        }
    }

    @Nullable
    private View inflateView(String name, Context context, AttributeSet attrs) {
        if (name.endsWith("TextInputEditText")) {
            return new MeizuTextInputEditText(context, attrs);
        }
        return null;
    }
}

And registering that custom interceptor is done just as in the docs by setting up a ViewPump on the onCreate of your activity:

@Override
@CallSuper
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ViewPump.Builder viewPumpBuilder = ViewPump.builder();
    if (isMeizuDevice()) {
        viewPumpBuilder.addInterceptor(new TextInputEditTextInterceptor());
    }
    ViewPump.init(viewPumpBuilder.build());
}

As you can see I only inflate the MeizuTextInputEditText if a Meizu device is detected. That way the reflection is not triggered for devices that do not need it. Also this method is a base Activity class that I have from which every other activity extends in my project so every activity that is started in my project AND where the device is a Meizu will have the fix automatically!

Solution 5 - Android

Remove hint from xml: either from TextInputLayout or TextInputEditText.

For Material Components

<com.google.android.material.textfield.TextInputLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

    <com.google.android.material.textfield.TextInputEditText
            android:id="@+id/text_input_edit_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
</com.google.android.material.textfield.TextInputLayout>

For Design Support

<android.support.design.widget.TextInputLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

    <android.support.design.widget.TextInputEditText
            android:id="@+id/text_input_edit_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
</android.support.design.widget.TextInputLayout>

In you code set hint programmatically:

val myTextInputEditText = findViewById<TextInputEditText>(R.id.text_input_edit_text)
myTextInputEditText.hint = "Your hint"

Tested on Meizu M5S, Android 6.0

Solution 6 - Android

Adding the hint on both TextInputLayout and TextInputEditText fixed the crash for me:

    <android.support.design.widget.TextInputLayout
        android:id="@+id/text_input_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/login"
        app:hintAnimationEnabled="false">

        <android.support.design.widget.TextInputEditText
            android:id="@+id/text_input_edit_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/login" />
        </android.support.design.widget.TextInputLayout>

Finally reset the hint of the TextInputEditText programmatically to avoid the very dark color of the hint text:

editText = findViewById(R.id.text_input_edit_text);
editText.setHint("");

Verified on Meizu MX6 with Android 6.0

Solution 7 - Android

I am using Kotlin and Fragments and I just recursively fixing all text inputs in onViewCreated.

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    fixTextInputEditText(view) // call this in onViewCreated
}

private fun fixTextInputEditText(view: View) {
    val manufacturer = Build.MANUFACTURER.toUpperCase(Locale.US)
    if ("MEIZU" in manufacturer) {
        val views = getAllTextInputs(view)
        views.forEach(::hackFixHintsForMeizu)
    }
}

private fun getAllTextInputs(v: View): List<TextInputEditText> {
    if (v !is ViewGroup) {
        val editTexts = mutableListOf<TextInputEditText>()
        (v as? TextInputEditText)?.let {
            editTexts += it
        }
        return editTexts
    }

    val result = mutableListOf<TextInputEditText>()
    for (i in 0 until v.childCount) {
        val child = v.getChildAt(i)
        result += getAllTextInputs(child)
    }
    return result
}

private fun hackFixHintsForMeizu(editText: TextInputEditText) {
    if (editText.hint != null) {
        editText.setHintTextColor(Color.TRANSPARENT)
        editText.hint = editText.hint
    }
}

Solution 8 - Android

This fix is now included in the new material-components release here: https://github.com/material-components/material-components-android/releases/tag/1.1.0-alpha09

Solution 9 - Android

None of variants above worked for me without modifications.

My app uses fragments, TextInputEditText sometimes being used without TextInputLayout, upgrading to latest AndroidX was not option at this time, replacing TextInputEditText was also not an option at this time.

My version (based on those solution and Google's fix):

import android.os.Build
import java.util.*
import android.content.Context
import android.support.design.widget.TextInputEditText
import android.util.AttributeSet
import android.widget.TextView
import android.support.design.widget.TextInputLayout
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import java.lang.reflect.Field
import java.lang.reflect.Method
import android.support.design.R

class MyInputEditText(context: Context?, attrs: AttributeSet?,defStyleAttr:Int) : TextInputEditText(context, attrs,defStyleAttr){

    constructor(context: Context?, attrs: AttributeSet?):this(context,attrs,R.attr.editTextStyle)
    constructor(context: Context?):this(context,null,R.attr.editTextStyle)


    private val buggyMeizu = ("meizu") in Build.MANUFACTURER.toLowerCase(Locale.US)

    private lateinit var getTextInputLayoutMethod:Method
    private lateinit var providesHintMethod:Method
    private lateinit var mHintField:Field

    init {
        if (buggyMeizu) {
            getTextInputLayoutMethod=TextInputEditText::class.java.getDeclaredMethod("getTextInputLayout")
            getTextInputLayoutMethod.isAccessible=true

            providesHintMethod=TextInputLayout::class.java.getDeclaredMethod("isProvidingHint")
            providesHintMethod.isAccessible=true

            mHintField=TextView::class.java.getDeclaredField("mHint")
            mHintField.isAccessible=true
        }
    }


    private fun getTILProvidesHint():Boolean {
        val layout=getTIL()
        if (layout!=null) {
            val result=providesHintMethod.invoke(layout) as Boolean
            return result;
        } else {
            return false
        }
    }

    private fun getTIL():TextInputLayout? = getTextInputLayoutMethod.invoke(this) as TextInputLayout?

    private fun getBaseHint():CharSequence? = mHintField.get(this) as CharSequence?

    override fun getHint(): CharSequence? {
        if (!buggyMeizu) {
            return super.getHint()
        } else {
            val layout=getTIL()
            return if (layout != null && (getTILProvidesHint()) ) 
                layout.hint
            else 
                provideHintWrapped()
        }
    }


    override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
        val needHint=(outAttrs.hintText==null)
        val ic = super.onCreateInputConnection(outAttrs)
        if (buggyMeizu) {
            if (ic != null && needHint) {
                outAttrs.hintText = this.provideHintWrapped()
            }
        }
        return ic
    }

    private fun provideHintWrapped():CharSequence? {

        val hintFromLayout=getHintFromLayoutMine()
        if (hintFromLayout!=null) {
            return hintFromLayout
        } else {
            val baseHint=getBaseHint()
            if (baseHint!=null) {
                return baseHint
            } else {
                return null
            }
        }

    }
    private fun getHintFromLayoutMine(): CharSequence? {
        val layout = getTIL()
        return layout?.hint
    }

    override fun onAttachedToWindow() {

        if (buggyMeizu) {

            val baseHint=getBaseHint()

            if (getTIL() != null
                    && getTILProvidesHint()
                    && baseHint == null) {
                this.hint=""
            }
        }

        super.onAttachedToWindow()
    }
}

After that find-and-replace TextInputEditText with MyInputEditText in all layout and code files.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionTurboblasterView Question on Stackoverflow
Solution 1 - AndroidTurboblasterView Answer on Stackoverflow
Solution 2 - AndroidAndreas WengerView Answer on Stackoverflow
Solution 3 - AndroidCarmenView Answer on Stackoverflow
Solution 4 - AndroiddirkvranckaertView Answer on Stackoverflow
Solution 5 - AndroidlocalhostView Answer on Stackoverflow
Solution 6 - Androiderkan.molla.devView Answer on Stackoverflow
Solution 7 - AndroidAndrewView Answer on Stackoverflow
Solution 8 - AndroidwaseefakhtarView Answer on Stackoverflow
Solution 9 - AndroiddkzmView Answer on Stackoverflow