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.
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,
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:
indexFirstUnsolvedPuzzle()
, just for Android Studio not to botheronClickListener
which launched a dialog, just because your app has dialogs.PuzzleSelection
Activity that randomly selects a level launches SolvePuzzle
at onResume()onBackPressed()
on both Activities, just to be able to log the back press.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:
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.
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;
...
}
/* ... */
}
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;
}
In the fragment, retrieve the new arguments instead of the array index.
That should do it.
the problem is in those line
static AppSectionsPagerAdapter mAppSectionsPagerAdapter;
static ViewPager mViewPager;
Why did you make it static?
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With