Android - android.database.StaleDataException during Activity destruction

599 views Asked by At

I'm facing this crash in my app for now in only two devices, both Samsung Galaxy (SM-G950F [api 26] & Galaxy J7 [api 23]).
I think the fact they are Samsung is pretty important because with all the other testing devices I use (physical, LG [api 24], and emulators by Genymotion) it has never happened.

This is the complete stack trace:

Fatal Exception: java.lang.RuntimeException: Unable to destroy activity {us.highlanders.app/rs.highlande.highlanders_app.activities_and_fragments.activities_create_post.CreatePostActivityMod}: android.database.StaleDataException: Attempted to access a cursor after it has been closed.
   at android.app.ActivityThread.performDestroyActivity(ActivityThread.java:4603)
   at android.app.ActivityThread.handleDestroyActivity(ActivityThread.java:4621)
   at android.app.ActivityThread.-wrap5(Unknown Source)
   at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1757)
   at android.os.Handler.dispatchMessage(Handler.java:105)
   at android.os.Looper.loop(Looper.java:164)
   at android.app.ActivityThread.main(ActivityThread.java:6938)
   at java.lang.reflect.Method.invoke(Method.java)
   at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:327)
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1374)

Caused by android.database.StaleDataException: Attempted to access a cursor after it has been closed.
   at android.database.BulkCursorToCursorAdaptor.throwIfCursorIsClosed(BulkCursorToCursorAdaptor.java:63)
   at android.database.BulkCursorToCursorAdaptor.getCount(BulkCursorToCursorAdaptor.java:69)
   at android.database.CursorWrapper.getCount(CursorWrapper.java:60)
   at <MY-APP-PACKAGE>.adapters.CustomGalleryAdapter.getItemCount(CustomGalleryAdapter.java:111)
   at android.support.v7.widget.RecyclerView$LayoutManager.getColumnCountForAccessibility(RecyclerView.java:10131)
   at android.support.v7.widget.RecyclerView$LayoutManager.onInitializeAccessibilityNodeInfo(RecyclerView.java:9992)
   at android.support.v7.widget.RecyclerView$LayoutManager.onInitializeAccessibilityNodeInfo(RecyclerView.java:9951)
   at android.support.v7.widget.RecyclerViewAccessibilityDelegate.onInitializeAccessibilityNodeInfo(RecyclerViewAccessibilityDelegate.java:61)
   at android.support.v4.view.AccessibilityDelegateCompat$AccessibilityDelegateApi16Impl$1.onInitializeAccessibilityNodeInfo(AccessibilityDelegateCompat.java:126)
   at android.view.View.onInitializeAccessibilityNodeInfo(View.java:7951)
   at android.view.View.createAccessibilityNodeInfoInternal(View.java:7912)
   at android.view.View$AccessibilityDelegate.createAccessibilityNodeInfo(View.java:27397)
   at android.view.View.createAccessibilityNodeInfo(View.java:7895)
   at android.view.accessibility.AccessibilityRecord.setSource(AccessibilityRecord.java:146)
   at android.view.accessibility.AccessibilityRecord.setSource(AccessibilityRecord.java:119)
   at android.view.View.onInitializeAccessibilityEventInternal(View.java:7849)
   at android.view.View$AccessibilityDelegate.onInitializeAccessibilityEvent(View.java:27280)
   at android.support.v4.view.AccessibilityDelegateCompat.onInitializeAccessibilityEvent(AccessibilityDelegateCompat.java:309)
   at android.support.v7.widget.RecyclerViewAccessibilityDelegate.onInitializeAccessibilityEvent(RecyclerViewAccessibilityDelegate.java:67)
   at android.support.v4.view.AccessibilityDelegateCompat$AccessibilityDelegateApi16Impl$1.onInitializeAccessibilityEvent(AccessibilityDelegateCompat.java:120)
   at android.view.View.onInitializeAccessibilityEvent(View.java:7835)
   at android.view.View.sendAccessibilityEventUncheckedInternal(View.java:7699)
   at android.view.View$AccessibilityDelegate.sendAccessibilityEventUnchecked(View.java:27219)
   at android.support.v4.view.AccessibilityDelegateCompat.sendAccessibilityEventUnchecked(AccessibilityDelegateCompat.java:248)
   at android.support.v4.view.AccessibilityDelegateCompat$AccessibilityDelegateApi16Impl$1.sendAccessibilityEventUnchecked(AccessibilityDelegateCompat.java:148)
   at android.view.View.sendAccessibilityEventUnchecked(View.java:7682)
   at android.support.v7.widget.RecyclerView.sendAccessibilityEventUnchecked(RecyclerView.java:3421)
   at android.view.View$SendViewStateChangedAccessibilityEvent.run(View.java:27434)
   at android.view.View$SendViewStateChangedAccessibilityEvent.runOrPost(View.java:27467)
   at android.view.View.notifyViewAccessibilityStateChangedIfNeeded(View.java:11870)
   at android.view.View.onFocusChanged(View.java:7544)
   at android.view.View.clearFocusInternal(View.java:7404)
   at android.view.View.unFocus(View.java:7437)
   at android.view.ViewGroup.unFocus(ViewGroup.java:1084)
   at android.view.ViewGroup.unFocus(ViewGroup.java:1086)
   at android.view.ViewGroup.removeViewInternal(ViewGroup.java:5416)
   at android.view.ViewGroup.removeViewInternal(ViewGroup.java:5403)
   at android.view.ViewGroup.removeView(ViewGroup.java:5334)
   at android.support.v4.view.ViewPager.removeView(ViewPager.java:1501)
   at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1529)
   at android.support.v4.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1759)
   at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1827)
   at android.support.v4.app.FragmentManagerImpl.dispatchStateChange(FragmentManager.java:3244)
   at android.support.v4.app.FragmentManagerImpl.dispatchDestroy(FragmentManager.java:3235)
   at android.support.v4.app.FragmentController.dispatchDestroy(FragmentController.java:265)
   at android.support.v4.app.FragmentActivity.onDestroy(FragmentActivity.java:390)
   at android.support.v7.app.AppCompatActivity.onDestroy(AppCompatActivity.java:209)
   at <MY-APP-PACKAGE>.HLActivity.onDestroy(HLActivity.java:164)
   at <MY-APP-PACKAGE>.CreatePostActivityMod.onDestroy(CreatePostActivityMod.java:95)
   at android.app.Activity.performDestroy(Activity.java:7462)
   at android.app.Instrumentation.callActivityOnDestroy(Instrumentation.java:1255)
   at android.app.ActivityThread.performDestroyActivity(ActivityThread.java:4590)
   at android.app.ActivityThread.handleDestroyActivity(ActivityThread.java:4621)
   at android.app.ActivityThread.-wrap5(Unknown Source)
   at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1757)
   at android.os.Handler.dispatchMessage(Handler.java:105)
   at android.os.Looper.loop(Looper.java:164)
   at android.app.ActivityThread.main(ActivityThread.java:6938)
   at java.lang.reflect.Method.invoke(Method.java)
   at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:327)
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1374)

What I basically have here is an Activity consisting of a ViewPager anchored at the bottom of the screen, where the first fragment is a RecyclerView with horizontal LayoutManager that wants to mimic a Telegram/Messenger picture gallery. Everything is handled by the class CustomGalleryAdapter (extending RecyclerView.Adapter) which uses a Cursor to fetch the media files from the device, and which crashes when, for accessibility reasons, during the destruction of the Activity, the cursor is referenced again. This Activity is created waiting for its result, so when I'm done I call finish() as usual to pass the result.

The fragment with the RecyclerView listens for the Loader callbacks:

@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    String[] projection = {
            MediaStore.Files.FileColumns._ID,
            MediaStore.Files.FileColumns.DATE_ADDED,
            MediaStore.Files.FileColumns.DATA,
            MediaStore.Files.FileColumns.MEDIA_TYPE
    };
    String selection = MediaStore.Files.FileColumns.MEDIA_TYPE + "="
            + MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE
            + " OR "
            + MediaStore.Files.FileColumns.MEDIA_TYPE + "="
            + MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO;
    String sortOrder = String.format("%s limit 50 ", MediaStore.Files.FileColumns.DATE_ADDED +" DESC");

    if (getActivity() != null) {
        return new CursorLoader(
                getActivity(),
                MediaStore.Files.getContentUri("external"),
                projection,
                selection,
                null,
                sortOrder
        );
    }
    else return null;
}

@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
    galleryAdapter.changeCursor(data);
}

@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
    galleryAdapter.changeCursor(null);
}`  

And looking at the framework sourcecode which calls back my cursor when it's already closed:

public int getColumnCountForAccessibility(Recycler recycler, State state) {
        if (mRecyclerView == null || mRecyclerView.mAdapter == null) {
            return 1;
        }
        return canScrollHorizontally() ? mRecyclerView.mAdapter.getItemCount() : 1;
    }  

which calls getItemCount() method:

@Override
public int getItemCount() {
    return (myCursor == null) ? 0 : myCursor.getCount();
}  

I thought that I could set the mAdapter field to null in the onDestroyView() method of the fragment:

@Override
public void onDestroyView() {
    super.onDestroyView();

    if (galleryRecView != null)
        galleryRecView.setAdapter(null);
    galleryAdapter = null;
}  

to solve the issue. But the app kept crashing when I tried to test with the Samsung Test Lab.

But the very odd thing is that I tried to debug on my devices those accessibility lines weren't even caught by the breakpoints...

Does anyone have a suggestion?

1

There are 1 answers

1
Vitaliy On

Try the following workaround:

@Override
public int getItemCount() {
    return (myCursor == null || myCursor.isClosed()) ? 0 : myCursor.getCount();
}