Search code examples
androidnullpointerexceptiontextviewandroid-viewpagerandroid-widget

TextView in ViewPager fragments randomly crashes the app NullPointerException: Attempt to invoke virtual method void View.sendAccessibilityEventUnchec


Summary

  1. First I describe the context, i.e. what I'm trying to do

  2. Second I explain you in which cases I encounter the bug and what is it (it's the current behavior)

  3. Third, I explain you the expected behavior

  4. Then I explain you how to reproduce the bug

  5. Finally I show you the minimal and executable code that can lead to the bug


Context

I am using the widget AutoScrollTextView available between others in the API https://github.com/ronghao/AutoScrollTextView. AutoScrollTextView is a TextView that can automatically autoscroll vertically and moreover, if the text is too long, horizontally. So an AutoScrollTextView can contain at least one text to be automatically scrolled (see the illustration in the GitHub repository).

Current behavior: the error

  1. If I use one AutoScrollTextView in the main activity, I don't see any bug. If I use it in the fragment of the main activity, I think it would be the case too (I didn't test it).
  2. If I use one AutoScrollTextView in each of at least two fragments of a ViewPager (2 instances of this same fragment class are used for the Viewpager), with at least one text to be automatically scrolled by each AutoScrollTextView, the app crashes with the following error (NB: this ViewPager belongs to the main activity):

E/AndroidRuntime: FATAL EXCEPTION: main Process: com.example.myapplication, PID: 2814 java.lang.NullPointerException: Attempt to invoke virtual method 'void android.view.View.sendAccessibilityEventUnchecked(android.view.accessibility.AccessibilityEvent)' on a null object reference at android.view.ViewRootImpl$SendWindowContentChangedAccessibilityEvent.run(ViewRootImpl.java:9304) at android.os.Handler.handleCallback(Handler.java:789) at android.os.Handler.dispatchMessage(Handler.java:98) at android.os.Looper.loop(Looper.java:164) at android.app.ActivityThread.main(ActivityThread.java:6944) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:327) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1374)

The error, however, is difficult to trigger:

  1. Sometimes, it's triggered as soon as the fragment of the main activity is started
  2. Sometimes, there is a random delay before it's triggered in the fragment of the main activity
  3. Sometimes, maybe it's not triggered (or I close the app before it would)

As you can see, there isn't any indication of the line that causes this crash.

What I've done to debug

  1. I have taken the library, and isolated the minimal part of it that lead to this bug.

  2. I have set up the minimal executable app with only the main activity, the ViewPager, and the fragment (2 instances of this same fragment class are used for the Viewpager).

  3. I have set up this minimal executable app with the isolated part of the library. You will find the sources below (even if there are "many" files, each one has been minimalized and contains only the strict minimal lines in order to make you able to reproduce the bug). Also: with the sources I give you, it's totally normal that you don't see text scrolling (nor text at all). It's because I've completely isolated the part of the API that triggers the bug, and the scrolling text wasn't related to this error, so it's not part of my isolated sources.

  4. I have tried to use Android's Timer instead of an Handler but the error still occurres.

  5. MarqueeTextView::scrollTo(currentScrollPos, 0); seems to trigger this error.

Expected behavior

Normally, the app of course should not crash. In other words, there shouldn't be any triggered error (especially the one I've quoted above). With the sources I give you, it's totally normal that you don't see text scrolling (nor text at all). It's because I've completely isolated the part of the API that triggers the bug, and the scrolling text wasn't related to this error, so it's not part of my isolated sources.

How to reproduce this bug

Below, I provide you all the minimal and executable sources you need (even if there are "many" files, each one has been minimalized and contains only the strict minimal lines in order to make you able to reproduce the bug). After having created the corresponding files and copy/paste these sources in them, run this minimal and executable app several times. Even if the error will or won't be shown for each run, normally you would see it at least one time after 5 runs max. You can use your smartphone to test this app, or maybe an Android Studio's emulator (but I didn't test on an emulator).

NB: with the sources I give you, it's totally normal that you don't see text scrolling (nor text at all). It's because I've completely isolated the part of the API that triggers the bug, and the scrolling text wasn't related to this error, so it's not part of my isolated sources.

My question

Could you tell me why this bug occures (I was unable to debug it) and why it seems so random? How could I correct it?

Minimal and executable app's sources

Short presentation

  1. First, I will give you the sources of the isolated part of the library

  2. Then, I will give you the sources of the app with the ViewPager, etc.

  3. Even if there are "many" files, each one has been minimalized and contains only the strict minimal lines in order to make you able to reproduce the bug. With the sources I give you, it's totally normal that you don't see text scrolling (nor text at all). It's because I've completely isolated the part of the API that triggers the bug, and the scrolling text wasn't related to this error, so it's not part of my isolated sources.

Isolated part of the library that triggers the bug

com.example.myapplication.libs.autoscrolling_text_view.MarqueeTextView.java

package com.example.myapplication.libs.autoscrolling_text_view;

import android.content.Context;

import androidx.appcompat.widget.AppCompatTextView;

import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 *
 * @author haohao on 2017/9/21 下午 02:33
 * @version v1.0
 */
public class MarqueeTextView extends AppCompatTextView {

    private int currentScrollPos = 0;

    ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);

    final TimerTask task = new TimerTask() {
        @Override
        public void run() {
            currentScrollPos += 1;
            scrollTo(currentScrollPos, 0);
        }
    };



public MarqueeTextView(Context context) {
        super(context);
    }

    public void postStartScroll(int delay) {  // Bug when reset AND then scheduleAtFixedRate are executed
        int speed = 6;
        pool.scheduleAtFixedRate(task, delay, speed, TimeUnit.MILLISECONDS);
    }
}

com.example.myapplication.libs.autoscrolling_text_view.MarqueeSwitcher.java

package com.example.myapplication.libs.autoscrolling_text_view;

import android.content.Context;
import android.util.AttributeSet;
import android.widget.RelativeLayout;
import android.widget.ViewSwitcher;

/**
 * MarqueeSwitcher {@link android.widget.TextSwitcher} t
 *
 * @author haohao on 2017/9/21 下午 03:57
 * @version v1.0
 */
public class MarqueeSwitcher extends ViewSwitcher {

    /**
     * Creates a new empty TextSwitcher for the given context and with the
     * specified set attributes.
     *
     * @param context the application environment
     * @param attrs a collection of attributes
     */
    public MarqueeSwitcher(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * Sets the text of the next view and switches to the next view. This can
     * be used to animate the old text out and animate the next text in.
     *
     */
    public void setText() {
        final MarqueeTextView t = getNextView();
        t.postStartScroll(1500);  // BUG HERE (Cf. MarqueeTextView::postStartScroll)
    }

    public MarqueeTextView getCurrentView() {
        return (MarqueeTextView) ((RelativeLayout) super.getCurrentView()).getChildAt(0);
    }

    public MarqueeTextView getNextView() {
        return (MarqueeTextView) ((RelativeLayout) super.getNextView()).getChildAt(0);
    }
}

com.example.myapplication.libs.autoscrolling_text_view.BaseScrollTextView.java

package com.example.myapplication.libs.autoscrolling_text_view;

import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.View;
import android.widget.RelativeLayout;
import android.widget.ViewSwitcher;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;

/**
 * 父类:上下滚动
 *
 * @author haohao on 2017/9/21 下午 02:28
 * @version v1.0
 */
public class BaseScrollTextView extends MarqueeSwitcher
        implements ViewSwitcher.ViewFactory {

    private static final int FLAG_START_AUTO_SCROLL = 1000;
    private ArrayList<String> textList;
    private Handler handler;

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

    private void init() {
        textList = new ArrayList<>();
        handler = new MyHandler(this);
        setFactory(this);
    }

    /**
     * 设置数据源
     */
    public void setTextList(List<String> titles) {
        textList.addAll(titles);
    }

    /**
     * 开始轮播
     */
    public void startAutoScroll() {
        handler.sendEmptyMessage(FLAG_START_AUTO_SCROLL);
    }

    @Override
    public View makeView() {
        RelativeLayout layout = new RelativeLayout(getContext());
        MarqueeTextView textView = new MarqueeTextView(getContext());
        layout.addView(textView);
        return layout;
    }

    private static class MyHandler extends Handler {
        WeakReference<BaseScrollTextView> textViewWeakReference;

        private MyHandler(BaseScrollTextView autoScrollTextView) {
            textViewWeakReference = new WeakReference<>(autoScrollTextView);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (null != textViewWeakReference) {
                BaseScrollTextView autoScrollTextView = textViewWeakReference.get();
                if (msg.what == FLAG_START_AUTO_SCROLL) {
                    if (autoScrollTextView.textList.size() > 0) {
                        autoScrollTextView.setText();
                    }
                }
            }
        }
    }
}

res.anim.push_up_in.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:shareInterpolator="false"
    android:zAdjustment="top">
    <translate
        android:duration="400"
        android:fromYDelta="100%"
        android:interpolator="@android:anim/accelerate_interpolator"
        android:toYDelta="0"/>
    <alpha
        android:duration="400"
        android:fromAlpha="0.0"
        android:toAlpha="1.0"/>
</set>

res.anim.push_up_out.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:shareInterpolator="false"
    android:zAdjustment="bottom">
    <translate
        android:duration="400"
        android:fromYDelta="0"
        android:interpolator="@android:anim/accelerate_interpolator"
        android:toYDelta="-100%"/>

    <alpha
        android:duration="400"
        android:fromAlpha="1.0"
        android:toAlpha="0.0"/>
</set>

The app (one activity, one fragment used two times in the activity's Viewpager ; the fragment uses AutoScrollTextView which randomly triggers the bug)

App-Level Gradle

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28


    defaultConfig {
        applicationId "com.example.myapplication"
        minSdkVersion 22
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation("com.google.guava:guava:28.2-android")
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

com.example.myapplication.MainActivity.java

package com.example.myapplication;

import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.viewpager.widget.ViewPager;

import com.google.common.collect.Lists;

import java.util.List;

public class MainActivity extends AppCompatActivity {
    private List<FragmentHomeSlide> slides;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final FragmentHomeSlide slide_1 = new FragmentHomeSlide();
        final FragmentHomeSlide slide_2 = new FragmentHomeSlide();
        slides = Lists.newArrayList(slide_1, slide_2);

        final ViewPager view_pager = findViewById(R.id.view_pager);
        assert getFragmentManager() != null;
        view_pager.setAdapter(new FragmentStatePagerAdapter(getSupportFragmentManager(), FragmentStatePagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
            @Override
            @NonNull
            public Fragment getItem(final int position) {
                return slides.get(position);
            }

            @Override
            public int getCount() {
                return slides.size();
            }
        });
    }
}

com.example.myapplication.FragmentHomeSlide.java

package com.example.myapplication;

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;

import com.example.myapplication.libs.autoscrolling_text_view.BaseScrollTextView;

import java.util.ArrayList;
import java.util.Arrays;

public class FragmentHomeSlide extends Fragment {
    @Override
    public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) {
        View inflated = inflater.inflate(R.layout.home_slide, container, false);
        setWidgets(inflated);
        return inflated;
    }

    private void setWidgets(View inflated) {
        String[] text_presentation = new String[1];
        text_presentation[0] = "Foo";

        BaseScrollTextView baseScrollTextView = inflated.findViewById(R.id.main_autoscroll_text1);
        baseScrollTextView.setTextList(new ArrayList<>(Arrays.asList(text_presentation)));
        baseScrollTextView.startAutoScroll();
    }
}

res.layout.activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        />

</androidx.constraintlayout.widget.ConstraintLayout>

res.layout.home_slide.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.myapplication.libs.autoscrolling_text_view.BaseScrollTextView
        android:id="@+id/main_autoscroll_text1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        />

</androidx.constraintlayout.widget.ConstraintLayout>

Solution

  • I think I have solved the problem by using runOnUiThread in the method MarqueeTextView::TimerTask::run. Since this change, I didn't see any bug. But I still don't know why this bug occurred, and why this modification solved it (I know it's better to use runOnUiThread in this situation since scrollTo is used, but I don't know what is the relationship between this call and the bug). If someone could comment my answer, to explain me, please :-) .

    You can compare the changes (presented below) with the original file in my question.

    So the only changes I've made are contained in the following file (com.example.myapplication.libs.autoscrolling_text_view.MarqueeTextView):

    package com.example.myapplication.libs.autoscrolling_text_view;
    
    import android.content.Context;
    
    import androidx.appcompat.app.AppCompatActivity;
    import androidx.appcompat.widget.AppCompatTextView;
    
    import java.util.TimerTask;
    import java.util.concurrent.Executors;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.TimeUnit;
    
    /**
     *
     * @author haohao on 2017/9/21 下午 02:33
     * @version v1.0
     */
    public class MarqueeTextView extends AppCompatTextView {
    
        private int currentScrollPos = 0;
    
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
    
        final TimerTask task = new TimerTask() {
            @Override
            public void run() {
                ((AppCompatActivity) getContext()).runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        currentScrollPos += 1;
                        scrollTo(currentScrollPos, 0);
                    }
                });
            }
        };
    
        public MarqueeTextView(Context context) {
            super(context);
        }
    
        public void postStartScroll(int delay) {  // Bug when reset AND then scheduleAtFixedRate are executed
            int speed = 6;
            pool.scheduleAtFixedRate(task, delay, speed, TimeUnit.MILLISECONDS);
        }
    }