Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mutation of a Bundle object

I'm working with the legacy code and I found an inconsistent behavior in this function:

@Override
public void openFragment(final Class<? extends BaseFragment> fragmentClass,
                         final boolean addToBackStack,
                         final Bundle args)
{
    long delay = 0;
    if (mDrawerLayout.isDrawerOpen(GravityCompat.START)) {
        delay = getResources().getInteger(android.R.integer.config_shortAnimTime) * 2;
    }
    // FIXME: quick fix, but not all cases
    final Bundle args666 = args != null ? (Bundle) args.clone() : null;
    new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            doOpenFragment(fragmentClass, addToBackStack, args666);
        }
    }, delay);
    closeDrawer();
}


protected void doOpenFragment(final Class<? extends BaseFragment> fragmentClass,
                              final boolean addToBackStack,
                              final Bundle args)
{
    try {
        if (getSupportFragmentManager().getBackStackEntryCount() >= 1) {
            showNavigationIcon();
        }
        hideKeyboard();
        BaseFragment fragment = createFragment(fragmentClass, args);
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        fragment.initTransactionAnimation(transaction);
        String tag = getTag(fragment);
        transaction.add(R.id.container, fragment, tag);
        if (addToBackStack) {
            transaction.addToBackStack(tag);
        }
        transaction.commitAllowingStateLoss();
        hideLastFragment(0);
    } catch (Exception e) {
        Sentry.captureException(e, "Error opening fragment");
    }
}

openFragment gets non-empty Bundle args, but doOpenFragment will get empty Bundle. Fragments are committed by calling commitAllowingStateLoss()

A quick fix can be to use Bundle.clone():

    final Bundle args666 = (Bundle) args.clone();
    new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            doOpenFragment(fragmentClass, addToBackStack, args666);
        }
    }, delay);

It will not handle all cases and deepCopy is available only in api26.

  1. Why does it happen?
  2. How to fix it?

[UPDATE]

I played with @Pavel's solution and things get weirder

    final Bundle args666 = args != null ? cloneThroughSerialization(args) : args;
    final Bundle args777 = args != null ? (Bundle) args.clone() : args;

enter image description here

[UPDATE2]

Actually, the problem isn't with postDelayed call. Let's see the call stack:

enter image description here

in goRightToTheCollectionScreen the Bundle is created and packed (nothing suspicious, no mutation afterward).

I guess, the source of the problem in two calls inside openFragmentsChain:

public void openRootFragmentsChain(Class<? extends BaseFragment> fragmentClass,
                                   List<Class<? extends BaseFragment>> fragmentClasses,
                                   boolean addToBackStack,
                                   Bundle args)
{
    openFragmentsChain(fragmentClasses, addToBackStack, args);
    openFragment(fragmentClass, true, args);
}

public void openFragmentsChain(List<Class<? extends BaseFragment>> fragmentClasses,
                               boolean addToBackStack,
                               Bundle args)
{
    try {
        for (int i = 0; i < fragmentClasses.size(); i++) {
            FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
            BaseFragment fragment = createFragment(fragmentClasses.get(i), args);
            String tag = getTag(fragment);
            transaction.add(R.id.container, fragment, tag);
            if (addToBackStack) {
                transaction.addToBackStack(tag);
            }
            if (i != fragmentClasses.size() - 1) {
                transaction.hide(fragment);
            }
            transaction.commitAllowingStateLoss();
        }
        if (fragmentClasses.size() >= 1) {
            updateDrawer();
        }
    } catch (Exception e) {
        Sentry.captureException(e, "Error opening fragment chain");
    }
}
protected void doOpenFragment(final Class<? extends BaseFragment> fragmentClass,
                              final boolean addToBackStack,
                              final Bundle args)
{
    try {
        if (getSupportFragmentManager().getBackStackEntryCount() >= 1) {
            showNavigationIcon();
        }
        hideKeyboard();
        BaseFragment fragment = createFragment(fragmentClass, args);
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        fragment.initTransactionAnimation(transaction);
        String tag = getTag(fragment);
        transaction.add(R.id.container, fragment, tag);
        if (addToBackStack) {
            transaction.addToBackStack(tag);
        }
        transaction.commitAllowingStateLoss();
        hideLastFragment(0);
    } catch (Exception e) {
        Sentry.captureException(e, "Error opening fragment");
    }
}

protected BaseFragment createFragment(Class<? extends BaseFragment> fragmentClass, Bundle args) throws Exception {
    BaseFragment fragment = fragmentClass.newInstance();
    fragment.setHasOptionsMenu(true);
    fragment.setArguments(args);
    fragment.setNavigationHandler(BaseFragmentNavigatorActivity.this);
    fragment.setToolbar(mToolbar);
    fragment.setMenuLoadService(mMenuLoaderService);
    return fragment;
}
like image 922
Maxim G Avatar asked Aug 18 '17 09:08

Maxim G


2 Answers

  1. Some other code modifies the same Bundle before run() is called. Problem is in your code.
  2. You can deep clone through serialization.

        public static Bundle cloneThroughSerialization(@NonNull Bundle bundle) {
            Parcel parcel = Parcel.obtain();
            bundle.writeToParcel(parcel, 0);
    
            Bundle clonedBundle  = new Bundle();
            clonedBundle.readFromParcel(parcel);
    
            parcel.recycle();
            return clonedBundle;
        }
    
like image 70
Pavel Avatar answered Nov 09 '22 00:11

Pavel


The code you posted seems ok, so probably your bundle is modified elsewhere (even though your postDelayed delay is 0, the runnable will be executed slightly later and its possible you modify the bundle meanwhile). Try to execute it directly without postDelayed, to see if the problem still persists. You can post more of your code, maybe we can figure out where else you touch that bundle.

If nothing else helps, you can always copy the method from API26 to your code and use it (edge case - this seems a simple issue so you shouldn't have to)

like image 44
Nick Avatar answered Nov 09 '22 00:11

Nick