NullPointerException on Meizu devices in Editor.updateCursorPositionMz
AndroidTextviewMeizuAndroid 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 TextInputLayout
s. Simply replacing these TextInputEditText
s with AppCompatEditText
s 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:
-
programmatically reset the
TextInputEditText
's hint back to what it was set to originally from xml (by calling its overriddengetHint()
which returns the container's hint). -
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.