Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android java.lang.IllegalStateException with ViewPager

I've made a very simple Android puzzle app with a ViewPager that lets the user swipe through an array of puzzles. I'm seeing an error in production that I don't know how to reproduce or debug:

java.lang.IllegalStateException: 
  at android.support.v4.view.ViewPager.a (ViewPager.java:204)
  at android.support.v4.view.ViewPager.c (ViewPager.java:2)
  at android.support.v4.view.ViewPager.onMeasure (ViewPager.java:207)
  at android.view.View.measure (View.java:22002)
  at android.view.ViewGroup.measureChildWithMargins (ViewGroup.java:6580)
  at android.widget.FrameLayout.onMeasure (FrameLayout.java:185)
  at android.view.View.measure (View.java:22002)
  at android.view.ViewGroup.measureChildWithMargins (ViewGroup.java:6580)
  at android.support.v7.internal.widget.ActionBarOverlayLayout.onMeasure (ActionBarOverlayLayout.java:257)
  at android.view.View.measure (View.java:22002)
  at android.view.ViewGroup.measureChildWithMargins (ViewGroup.java:6580)
  at android.widget.FrameLayout.onMeasure (FrameLayout.java:185)
  at android.view.View.measure (View.java:22002)
  at android.view.ViewGroup.measureChildWithMargins (ViewGroup.java:6580)
  at android.widget.LinearLayout.measureChildBeforeLayout (LinearLayout.java:1514)
  at android.widget.LinearLayout.measureVertical (LinearLayout.java:806)
  at android.widget.LinearLayout.onMeasure (LinearLayout.java:685)
  at android.view.View.measure (View.java:22002)
  at android.view.ViewGroup.measureChildWithMargins (ViewGroup.java:6580)
  at android.widget.FrameLayout.onMeasure (FrameLayout.java:185)
  at com.android.internal.policy.DecorView.onMeasure (DecorView.java:721)
  at android.view.View.measure (View.java:22002)
  at android.view.ViewRootImpl.performMeasure (ViewRootImpl.java:2414)
  at android.view.ViewRootImpl.performTraversals (ViewRootImpl.java:2159)
  at android.view.ViewRootImpl.doTraversal (ViewRootImpl.java:1390)
  at android.view.ViewRootImpl$TraversalRunnable.run (ViewRootImpl.java:6762)
  at android.view.Choreographer$CallbackRecord.run (Choreographer.java:966)
  at android.view.Choreographer.doCallbacks (Choreographer.java:778)
  at android.view.Choreographer.doFrame (Choreographer.java:713)
  at android.view.Choreographer$FrameDisplayEventReceiver.run (Choreographer.java:952)
  at android.os.Handler.handleCallback (Handler.java:789)
  at android.os.Handler.dispatchMessage (Handler.java:98)
  at android.os.Looper.loop (Looper.java:169)
  at android.app.ActivityThread.main (ActivityThread.java:6595)
  at java.lang.reflect.Method.invoke (Method.java)
  at com.android.internal.os.Zygote$MethodAndArgsCaller.run (Zygote.java:240)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:767)

What should I do to figure out how to reproduce this error myself, and to figure out how to fix it? My android programming skills are fairly limited, and the stack trace above is extremely cryptic to me.

In case it's helpful, here is a second stack trace that appears to be related:

java.lang.IllegalStateException: 
  at android.support.v4.view.ViewPager.access$000 (ViewPager.java)
  or                     .access$200 (ViewPager.java)
  or                     .addNewItem (ViewPager.java)
  or                     .calculatePageOffsets (ViewPager.java)
  or                     .canScroll (ViewPager.java)
  or                     .completeScroll (ViewPager.java)
  or                     .determineTargetPage (ViewPager.java)
  or                     .distanceInfluenceForSnapDuration (ViewPager.java)
  or                     .executeKeyEvent (ViewPager.java)
  or                     .getChildRectInPagerCoordinates (ViewPager.java)
  or                     .infoForChild (ViewPager.java)
  or                     .initViewPager (ViewPager.java)
  or                     .isGutterDrag (ViewPager.java)
  or                     .onPageScrolled (ViewPager.java)
  or                     .onSecondaryPointerUp (ViewPager.java)
  or                     .populate (ViewPager.java)
  or                     .recomputeScrollPosition (ViewPager.java)
  or                     .scrollToItem (ViewPager.java)
  or                     .setCurrentItem (ViewPager.java)
  or                     .setCurrentItemInternal (ViewPager.java)
  or                     .smoothScrollTo (ViewPager.java)
  at android.support.v4.view.ViewPager.arrowScroll (ViewPager.java)
  or                     .populate (ViewPager.java)
  or                     .requestParentDisallowInterceptTouchEvent (ViewPager.java)
  at android.support.v4.view.ViewPager.onMeasure (ViewPager.java)
  at android.view.View.measure (View.java:19759)
  at android.view.ViewGroup.measureChildWithMargins (ViewGroup.java:6124)
  at android.widget.FrameLayout.onMeasure (FrameLayout.java:185)
  at android.view.View.measure (View.java:19759)
  at android.view.ViewGroup.measureChildWithMargins (ViewGroup.java:6124)
  at android.support.v7.internal.widget.ActionBarOverlayLayout.onMeasure (ActionBarOverlayLayout.java)
  at android.view.View.measure (View.java:19759)
  at android.view.ViewGroup.measureChildWithMargins (ViewGroup.java:6124)
  at android.widget.FrameLayout.onMeasure (FrameLayout.java:185)
  at android.view.View.measure (View.java:19759)
  at android.view.ViewGroup.measureChildWithMargins (ViewGroup.java:6124)
  at android.widget.LinearLayout.measureChildBeforeLayout (LinearLayout.java:1464)
  at android.widget.LinearLayout.measureVertical (LinearLayout.java:758)
  at android.widget.LinearLayout.onMeasure (LinearLayout.java:640)
  at android.view.View.measure (View.java:19759)
  at android.view.ViewGroup.measureChildWithMargins (ViewGroup.java:6124)
  at android.widget.FrameLayout.onMeasure (FrameLayout.java:185)
  at com.android.internal.policy.DecorView.onMeasure (DecorView.java:687)
  at android.view.View.measure (View.java:19759)
  at android.view.ViewRootImpl.performMeasure (ViewRootImpl.java:2283)
  at android.view.ViewRootImpl.performTraversals (ViewRootImpl.java:2036)
  at android.view.ViewRootImpl.doTraversal (ViewRootImpl.java:1258)
  at android.view.ViewRootImpl$TraversalRunnable.run (ViewRootImpl.java:6348)
  at android.view.Choreographer$CallbackRecord.run (Choreographer.java:871)
  at android.view.Choreographer.doCallbacks (Choreographer.java:683)
  at android.view.Choreographer.doFrame (Choreographer.java:619)
  at android.view.Choreographer$FrameDisplayEventReceiver.run (Choreographer.java:857)
  at android.os.Handler.handleCallback (Handler.java:751)
  at android.os.Handler.dispatchMessage (Handler.java:95)
  at android.os.Looper.loop (Looper.java:154)
  at android.app.ActivityThread.main (ActivityThread.java:6123)
  at java.lang.reflect.Method.invoke (Method.java)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run (ZygoteInit.java:867)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:757)

Should I post a shortened/simplified version of my ViewPager code?


Edit: Here's the class that does most of the work:

public class SolvePuzzle extends ActionBarActivity {

    // The code is longer than is shown here, but hopefully this is enough to be helpful
    static AppSectionsPagerAdapter mAppSectionsPagerAdapter;
    static ViewPager mViewPager;
    static int level;
    static String images[];
    static String levelDescriptions[];
    static String puzzles[];
    static String answers[];
    static String hints[];
    static String congratulationsArray[];

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_solve_puzzle);

        // Some code related to getSupportActionBar(); which I've cut out for brevity

        Intent intent = getIntent();
        if (intent != null) {
            level = intent.getIntExtra(PuzzleSelection.LEVEL, 0);  // Possible bug here with default level set to 0?
        }

        // Here there's code that populates images, levelDescriptions, puzzles, answers, and other arrays using the level integer
        // The arrays will have different lengths depending on the level
        // i.e. there is a different number of puzzles in each level
        // I do something along the lines of puzzles = getPuzzlesGivenLevel(level); and similarly for answers, hints, etc.

        mAppSectionsPagerAdapter = new AppSectionsPagerAdapter(getSupportFragmentManager());
        mViewPager = (ViewPager) findViewById(R.id.pager);
        mViewPager.setAdapter(mAppSectionsPagerAdapter);
        mViewPager.setOnPageChangeListener(new OnPageChangeListener() {
            @Override public void onPageScrollStateChanged(int arg0) {
            }
            @Override public void onPageScrolled(int arg0, float arg1, int arg2) {
            }
            @Override public void onPageSelected(int puzzleIndex) {
        callSetShareIntent(puzzles[puzzleIndex]);  // Make share button share the correct puzzle
            }
        });
        mViewPager.setCurrentItem(indexFirstUnsolvedPuzzle());
    }

    private int indexFirstUnsolvedPuzzle() {
    // Gets index of first unsolved puzzle in this level
    }

    private void callSetShareIntent(String puzzleStatement) {
    // Creates an Intent called shareIntent; and then calls setShareIntent(shareIntent);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                NavUtils.navigateUpFromSameTask(this);
                return true;
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.solve_puzzle, menu);
        MenuItem item = menu.findItem(R.id.menu_item_share);

        // Following http://stackoverflow.com/questions/19118051/unable-to-cast-action-provider-to-share-action-provider
        mShareActionProvider = (ShareActionProvider) MenuItemCompat.getActionProvider(item);
        if (mShareActionProvider == null) {
            // Following http://stackoverflow.com/questions/19358510/why-menuitemcompat-getactionprovider-returns-null
            mShareActionProvider = new ShareActionProvider(this);
            MenuItemCompat.setActionProvider(item, mShareActionProvider);
        }
        callSetShareIntent(puzzles[mViewPager.getCurrentItem()]);
        return true;  // Return true to display menu
    }

    private void setShareIntent(Intent shareIntent) {
        if (mShareActionProvider != null) {
            mShareActionProvider.setShareIntent(shareIntent);  // Should be called whenever new fragment is displayed
        }
    }

    public class AppSectionsPagerAdapter extends FragmentPagerAdapter {

        public AppSectionsPagerAdapter(FragmentManager fm) {
            super(fm);
        }

        @Override
        public Fragment getItem(int puzzleIndex) {
            Fragment fragment = new SolvePuzzleFragment();
            Bundle args = new Bundle();
            args.putInt(PUZZLE_INDEX, puzzleIndex);
            fragment.setArguments(args);
            return fragment;
        }

        @Override
        public int getCount() {
            return puzzles.length;
        }
    }

    public static class SolvePuzzleFragment extends Fragment implements OnClickListener {

        public double correctAnswer;
        public int puzzleIndex;

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            View rootView = inflater.inflate(R.layout.fragment_solve_puzzle, container, false);
            Bundle args = getArguments();
            puzzleIndex = args.getInt(PUZZLE_INDEX);

        // Sets a bunch of TextViews using the puzzleIndex
        // For example, get string in puzzles[puzzleIndex] and put it in a TextView, et cetera
        // Set a bunch of onClickListenters

    }

    // A bunch of functions for checking the user's answer, opening congratulations, etc
    // E.g. public void openCongratulationsAlert(View view) { ... }, public void openIncorrectAnswerToast() { ... }

    }

}

Please let me know if this needs more explanation.

like image 863
Adrian Avatar asked Mar 17 '18 06:03

Adrian


2 Answers

I installed your app in an emulator and was able to reproduce this crash with the monkey tool (https://developer.android.com/studio/test/monkey.html). This tool simulates user actions such as clicks, touch, rotations, etc., at a high speed. I couldn't reproduce it manually though.

You wrote in your question that you wanted to know how to figure out what the problem could be, so I will explain my process in detail. For the actual solution, skip to section 5.

1. How to know the message of the IllegalStateException

The first stack trace says the exception was thrown at ViewPager.java, so I searched for the string "throw new IllegalStateException" in that file. There are different versions, but the few I checked throw this exception in only 6 places,

  • two are about programmatic ViewPager drags, but your app doesn't do that, so I discarded these
  • two are about to not calling super methods within a class that inherits from ViewPager, but since in your code you're just using ViewPager, I discarded those too
  • one was in addView(), which does not appear on your stack traces, so I discarded that too.

The only one remaining is thrown at populate(int), and it had to be this one. But just to be sure, I checked the second stacktrace:

  • The fourth "at" line says ViewPager.onMeasure() was called. There are no "or" lines here, so this is for sure.

  • The third "at" line gives three options. Looking at the source, only populate() is ever called from onMeasure(), so the it had to be that one.

  • The second "at" line gives 21 options, but again looking at the source, populate() does just calls populate(int)... the same method as before, and so it all fits.

Here is the throwing code in populate(int):

throw new IllegalStateException("The application's PagerAdapter changed the adapter's"
    + " contents without calling PagerAdapter#notifyDataSetChanged!"
    + " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N
    + " Pager id: " + resName
    + " Pager class: " + getClass()
    + " Problematic adapter: " + mAdapter.getClass());

And this is why I answered this the first time.

2. Reproducing the bug

After downloading your app in an emulator, I ran this from the command line:

adb shell monkey -p atorch.statspuzzles -v --pct-rotation 20 100000

This sends the emulator 100.000 semi-random touch events, rotations, volume up/down, etc., at a very high speed, to test the app under stress. If you run this command with a debug build of your app, you should be able to see the stack trace I got in step 1.

3. Getting information about the bug (mostly speculative)

From here, you can put a lot of Log.d() lines in your code, run monkey, and that should give you an idea of what your users did to crash your app. Of course I can't do that, so I wrote a tiny app with the code you provided, and ran it with monkey to see if I could get something else. What I did was:

  1. Removed all references to your R class (like, replaced your layout with one of my own, with just a ViewPager, and replaced your Fragment layout with a simple View.
  2. Returned zero from indexFirstUnsolvedPuzzle(), just for Android Studio not to bother
  3. In the Fragment class I added an onClickListener which launched a dialog, just because your app has dialogs.
  4. Simulated level selection with a PuzzleSelection Activity that randomly selects a level launches SolvePuzzle at onResume()
  5. Added onBackPressed() on both Activities, just to be able to log the back press.
  6. I added Log.d() on every single method to be able to follow the process in logcat.

I ran this app with monkey and just before crashing I got this in the log:

(section 1)
SolvePuzzle@84e25c: back pressed
ViewPager{9256292}: scrolled, offset: 9.2589855E-4
AppSectionsPagerAdapter@4b49965: getCount() invoked, we have 3 elements
(section 2)
Creating Activity PuzzleSelection@580af24. Orientation: portrait
Starting puzzle for level 1
ViewPager{9256292}: scrolled, offset: 0.0
AppSectionsPagerAdapter@4b49965: getCount() invoked, we have 3 elements
AppSectionsPagerAdapter@4b49965: getCount() invoked, we have 3 elements
(section 3)
Creating Activity SolvePuzzle@16d7aaffor level 3. Orientation: portrait
Creating adapter AppSectionsPagerAdapter@958bebc with 2 elements.
AppSectionsPagerAdapter@958bebc: getCount() invoked, we have 2 elements
AppSectionsPagerAdapter@958bebc: getCount() invoked, we have 2 elements
ViewPager{c002954}: scrollStateChanged to 0
AppSectionsPagerAdapter@4b49965: getCount() invoked, we have 2 elements

4. Cause of the crash

Note that the adapter in section 1 (@4b49965) kept getting called in section 2, when SolvePuzzle was no longer showing on the screen. It was called even in section 3, after a new Adapter had been created. The result of getCount() is different in section 1 and section 3 for this adapter, which means that the adapter had changed its contents, and so the exception was thrown.

Most likely this adapter kept being used after its SolveActivity had been finished because its ViewPager was doing some housekeeping after becoming invisible. The problem is that the old Activity read an reference to a String array that had just been written by the new Activity, and this only happened because the arrays are static members of SolvePuzzle.

(As a side note, those static arrays indeed cause another bug in your app: if you choose level X, solve a problem there and go back to the menu through the congratulations dialog, then choose another level Y, and then press back twice, you end up in level Y, instead of level X.)

5. Solution

As a general rule, only use static fields when they are truly immutable, such as constants, or at least when you can synchronize concurrent access properly. Static classes, on the other hand, are almost always preferable to inner classes, becuse of memory leaks.

In your particular case, you should:

  1. Remove the static keyword from all your arrays (and really, from all your fields).

    AppSectionsPagerAdapter mAppSectionsPagerAdapter; ViewPager mViewPager; int level; String images[]; String levelDescriptions[]; String puzzles[]; String answers[]; String hints[]; String congratulationsArray[];

You can also make these fields private, though that's not strictly necessary.

  1. Make your adapter class static as well. Android Studio will complain that it can't find puzzles, so you can pass it via constructor, and also pass the other former static arrays.

    public static class AppSectionsPagerAdapter extends FragmentPagerAdapter {
    
        private String images[];
        private String puzzles[];
        ...
    
        public AppSectionsPagerAdapter(FragmentManager fm, 
            String[] puzzles, 
            String[] images, 
            ...
        ) {
            super(fm);
            this.puzzles = puzzles;
            this.images = images;
            ...
        }
    
        /* ... */ 
    }
    
  2. In getItem(), instead of passing Fragments the array index, pass them the specific values they need:

    @Override
    public Fragment getItem(int puzzleIndex) {
        Fragment fragment = new SolvePuzzleFragment();
        Bundle args = new Bundle();
        args.putString(PUZZLE_CONTENT, puzzles[puzzleIndex]);
        args.putString(PUZZLE_IMAGE, images[puzzleIndex]);
        ...
        fragment.setArguments(args);
        return fragment;
    }
    
  3. In the fragment, retrieve the new arguments instead of the array index.

That should do it.

like image 156
Leo supports Monica Cellio Avatar answered Nov 07 '22 07:11

Leo supports Monica Cellio


the problem is in those line static AppSectionsPagerAdapter mAppSectionsPagerAdapter; static ViewPager mViewPager;

Why did you make it static?

like image 25
Linh Nguyen Avatar answered Nov 07 '22 07:11

Linh Nguyen