73

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.

Turboblaster
  • 1,320
  • 1
  • 10
  • 11
  • 1
    Same for me. Compile/target sdk = 28, didn't have problem when compile/target was 27. – localhost Aug 17 '18 at 16:31
  • 3
    upd. Affected Meizu devices are on Android 6 and 7. – localhost Aug 17 '18 at 17:38
  • @localhost Mmh that's good to note, I've also had these issues since I updated to targetSdk 28. But I can't really downgrade to 27 now... – Turboblaster Aug 18 '18 at 13:06
  • 1
    I can confirm this issue for Meizu devices on Android 6 and 7 after updating my apps from SDK 27 to 28. – Steffen Aug 22 '18 at 06:38
  • I just upgraded to AndroidX and see this issue as well. Still running target sdk 25. I'm using com.google.android.material.textfield.TextInputEditText inside a com.google.android.material.textfield.TextInputLayout. Will try to replace with androidx.appcompat.widget.AppCompatEditText and see if it fixes the crash. Not an option to downgrade from AndroidX since that was a big refactor. – mliu Sep 28 '18 at 09:10
  • 1
    This problem was recorded in https://github.com/android-in-china/Compatibility/issues/11 with a workaround. Hope that helps. – qezt Oct 15 '18 at 02:15

9 Answers9

61

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.

Turboblaster
  • 1,320
  • 1
  • 10
  • 11
  • 2
    This solution actually does not work for me. I need some extension of AppCompatEditText inside TextInputLayout to make it work. Anyone has come up with a different workaround? – rui.mendes Aug 31 '18 at 07:46
  • 7
    This fix worked for me on AndroidX. I had a com.google.android.material.textfield.TextInputEditText inside a com.google.android.material.textfield.TextInputLayout. Replaced the EditText with a androidx.appcompat.widget.AppCompatEditText and the crash went away. – mliu Sep 29 '18 at 07:21
  • 2
    I found next solution: https://github.com/android-in-china/Compatibility/issues/11 – Andrey Nov 18 '18 at 13:03
  • This helped, thx. But my crash occurs while deleting all symbols in EditText. – lewkka Nov 26 '18 at 09:04
7

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

Andreas Wenger
  • 4,310
  • 1
  • 22
  • 31
  • 1
    According to the discussion in the link the fix should be released on next version around end of May 2019. (Current release is 1.1.0-alpha06, so whatever version comes next) – mgR May 19 '19 at 05:21
  • 2
    @MichaelGroenendijk it was released in 1.1.0-alpha07, see https://github.com/material-components/material-components-android/releases/tag/1.1.0-alpha07 – Vadim Kotov Jun 25 '19 at 15:51
5

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());
        }
    }
}
Carmen
  • 1,574
  • 17
  • 18
  • Can you share the code which describes the resetting of `TextInputEditText`'s hint back? And where do you call `hackFixHintsForMeizu` method? – Eugene Babich Mar 26 '19 at 11:12
  • @ZhebzhikBabich I'm not sure I understand your first question. If you want to know how the hint is removed from the `TextInputEditText`, you can look at the source of `TextInputLayout.setEditText()`. I updated the answer with a snippet and link. For the second question: we only have this problem in one screen, so this solution is ok for us. If you have lots of `TextInputEditText` throughout the app, you might need a different solution (replacing `TextInputEditText` with `AppCompatEditText` would be better if possible) – Carmen Mar 26 '19 at 19:06
  • Ok, thanks. I just thought that I need to clear hint by in overriding of getHint() method. – Eugene Babich Mar 27 '19 at 05:41
  • I just realized that part of my answer to your second question was omitted for some reason. We call this from the activity's `onCreate()` method: `hackFixHintsForMeizu(editText1, editText2, editText3, ...)`. We only need this hack in one screen. – Carmen Mar 27 '19 at 19:35
5

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!

dirkvranckaert
  • 1,364
  • 1
  • 17
  • 30
  • Thank you for this answer! Saved me a lot of time. I'm only wondering where exactly are you checking if the device is a Meizu, maybe you forgot to add it to your response or just mentioned you are doing it but elsewhere in your codebase (probably before registering this interceptor?) – Guilherme Santos Feb 14 '19 at 10:50
  • @GuilhermeSantos I just updated the answer with the viewpump init. It's setup as described in the docs, but only for Meizu devices. If you would need custom inflation interception for other devices as well (to potentially fix other stuff) you could easily add more if checks here and add the interceptors! – dirkvranckaert Feb 15 '19 at 12:01
2

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

localhost
  • 5,568
  • 1
  • 33
  • 53
  • Tried some cases with hints, and figured out that adding hint in XML on both (android.support.design.widget.TextInputLayout and android.support.design.widget.TextInputEditText) layouts fixes the crash. – Алексей Юр Nov 08 '18 at 12:45
  • localhost, in your code you set hint programmatically to TextInputEditText. Will it crash if you'll set hint to your TextInputLayout instead? – Eugene Babich Apr 04 '19 at 07:48
2

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

erkan.molla.dev
  • 170
  • 2
  • 11
  • At TextInputLayout add the real hint (android:hint="@string/login") and at TextInputEditText add empty hint (android:hint="") – wiz Jan 16 '19 at 09:02
  • This is not the correct solution. Using this fix will introduce more crashes due the the framework bug here --> https://stackoverflow.com/questions/45898228/android-attempt-to-invoke-virtual-method-void-android-view-view-getboundsonsc – Ray Li Feb 09 '19 at 18:10
1

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
    }
}
theme_an
  • 1,506
  • 14
  • 16
0x384c0
  • 2,256
  • 1
  • 17
  • 14
  • And what about xml? Where do you set the hint? In TextInputLayout or in TextInputEditText? – Eugene Babich Apr 04 '19 at 07:31
  • My approach does not require editing xml. Instead, this code recursively finds all editText and applying `hackFixHintsForMeizu` function to them. Easy to implement, but might slow down app, while fragment or activity loading. – 0x384c0 Apr 04 '19 at 08:22
  • I've meant that if I've set hint of TextInputLayout to "myHint" and didn't set hint for TextInputEditText in xml, then `editText.hint = editText.hint` will set null to null. Will this hack work in this case or will the app crash on meizu as usual? – Eugene Babich Apr 04 '19 at 08:31
  • From my practice, it works in this case. Crash occurs only when `hint != null` and `myHint == null`. But i think, adding nullability check for `hint` won't cause crash. If it will, i'll undo my answer edit. – 0x384c0 Apr 04 '19 at 08:53
1

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

waseefakhtar
  • 1,373
  • 2
  • 24
  • 47
0

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.

Tauri
  • 1,291
  • 1
  • 14
  • 28