5

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.

Adrian
  • 3,138
  • 2
  • 28
  • 39
  • 1
    Are you adding/removing items to `ViewPager` at runtime . If yes provide your code .' – ADM Mar 17 '18 at 06:22
  • @ADM I'm going to post an edited version of my code, please let me know if it needs more explanation. Thank you! – Adrian Apr 15 '18 at 01:37
  • Link to the app, in case that helps clarify what's going on: https://play.google.com/store/search?q=probability%20puzzles&c=apps (Probability Puzzles, should be the first result) – Adrian Apr 20 '18 at 02:49
  • 1
    where is your puzzles got init? and what is in your indexFirstUnsolvedPuzzle – Linh Nguyen Apr 21 '18 at 03:17
  • @Linh the puzzles array is set in onCreate (after getting the level from the intent), and its contents depend on the level. That code block has a bunch of if elses checking the level, so I replaced it with a short comment explaining what it does – Adrian Apr 21 '18 at 03:40
  • 1
    do you call mViewPager.setCurrentItem some where else? – Linh Nguyen Apr 21 '18 at 03:48

2 Answers2

7

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.

  • Thank you, I'll give that a try and see whether it helps. The app has a few levels, each with its own `puzzles` array. The main screen has a button for each level, which sends the user to `SolvePuzzle` with the level carried in the intent. – Adrian Apr 21 '18 at 00:47
  • I'm looking at this again and I'm confused: the only place where I set the `puzzles` array (and `answers` and other arrays) is in `onCreate` of `public class SolvePuzzle`. That means that `puzzles` can't change unless `onCreate` is called. Since `onCreate` also sets `mAppSectionsPagerAdapter = new AppSectionsPagerAdapter(getSupportFragmentManager());`, where should I be calling `mAppSectionsPagerAdapter.notifyDataSetChanged()`? – Adrian Apr 21 '18 at 03:04
  • 1
    Since you don't intend to modify your arrays after creating the adapter, you really don't need `notifyOnDateSetChanged()`. However your arrays do get modified without your knowing. Please check my updated anwer – Leo supports Monica Cellio Apr 22 '18 at 19:05
  • Thanks, man! I faced this issue on one of the projects I was asked to bugfix. The weirdest thing was that bug only reproduces on Xiaomi device and Samsung and happens on second rapid activity open (activity closed and opened again). Everything possible was done to eliminate the bug, but nothing helped. Except one small change - removing static modifier for list of the fragments passed to pager adapter. Looks like garbage collector was unable to clean the field and instead of 0 items list already was populated with 2 in my case. – Roman T. Nov 25 '20 at 12:28
2

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

Why did you make it static?

Linh Nguyen
  • 1,264
  • 1
  • 10
  • 22
  • Probably because I'm a very amateur Android developer. What sort of problem will that cause? The app "works" in the sense that users are able to navigate between levels and puzzles and input answers. What bug does that `static` cause and how can I reproduce it? – Adrian Apr 21 '18 at 03:57
  • because with static all SolvePuzzle instance share the same AppSectionsPagerAdapter and ViewPager, to reproduce the bug, try to open SolvePuzzle few time – Linh Nguyen Apr 21 '18 at 04:01
  • The app has a main menu with different buttons for each level. If you click on one, you get sent to SolvePuzzle with an intent carrying the level. If you then navigate back and open a different level, the app doesn't crash. Why is that? How do I make it crash? If you're willing to install and test it out, see https://play.google.com/store/apps/details?id=atorch.statspuzzles – Adrian Apr 21 '18 at 04:05
  • Ok I have an answer for you (as to why I made things `static`): inside `public static class SolvePuzzleFragment` I have a button that goes to the next puzzle (the user sees this button after they successfully answer a puzzle). The lines for the positive button action include `mViewPager.setCurrentItem(next_puzzleIndex);`. If I make mViewPager non-static, I get a "non-static field cannot be referenced from a static context" error in android studio – Adrian Apr 21 '18 at 04:28
  • For the sake of brevity I didn't show that button-related code in the code snippet in my question. Let me know if you'd like me to add it. – Adrian Apr 21 '18 at 04:30
  • Upvoted, because in my case static modifier messed things up – Roman T. Nov 25 '20 at 12:29