material-foundation / material-remixer-android

Remixer for Android. Live adjustment of app variables.
Apache License 2.0
774 stars 54 forks source link

Write tests for crashes on RemixerFragment.showRemixer() being called too quickly. #64

Open pingpongboss opened 7 years ago

pingpongboss commented 7 years ago

This is quite easy to do on a slow device (or emulator).

First attach RemixerFragment to a button

RemixerFragment remixerFragment = RemixerFragment.newInstance();
remixerFragment.attachToButton(this, (Button) findViewById(R.id.remixer_button));

Then tap on the button twice quickly. On a sufficiently slow device or emulator, you will get this crash:

11-02 18:16:56.269  6040  6040 E AndroidRuntime: FATAL EXCEPTION: main
11-02 18:16:56.269  6040  6040 E AndroidRuntime: Process: com.google.android.material.motion.family.rebound.sample, PID: 6040
11-02 18:16:56.269  6040  6040 E AndroidRuntime: java.lang.IllegalStateException: Fragment already added: RemixerFragment{2090b38 #0 Remixer}
11-02 18:16:56.269  6040  6040 E AndroidRuntime:    at android.support.v4.app.FragmentManagerImpl.addFragment(FragmentManager.java:1361)
11-02 18:16:56.269  6040  6040 E AndroidRuntime:    at android.support.v4.app.BackStackRecord.run(BackStackRecord.java:734)
11-02 18:16:56.269  6040  6040 E AndroidRuntime:    at android.support.v4.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:1677)
11-02 18:16:56.269  6040  6040 E AndroidRuntime:    at android.support.v4.app.FragmentManagerImpl$1.run(FragmentManager.java:536)
11-02 18:16:56.269  6040  6040 E AndroidRuntime:    at android.os.Handler.handleCallback(Handler.java:751)
11-02 18:16:56.269  6040  6040 E AndroidRuntime:    at android.os.Handler.dispatchMessage(Handler.java:95)
11-02 18:16:56.269  6040  6040 E AndroidRuntime:    at android.os.Looper.loop(Looper.java:154)
11-02 18:16:56.269  6040  6040 E AndroidRuntime:    at android.app.ActivityThread.main(ActivityThread.java:6077)
11-02 18:16:56.269  6040  6040 E AndroidRuntime:    at java.lang.reflect.Method.invoke(Native Method)
11-02 18:16:56.269  6040  6040 E AndroidRuntime:    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:865)
11-02 18:16:56.269  6040  6040 E AndroidRuntime:    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:755)

Remixer should be hardened against this simple error.

nicksahler commented 7 years ago

I've looked into this. It seems as though isAdded does not give accurate values for sufficiently fast calls of show (there's a delay).

I discovered this while building the requested shake feature ( #112 ) - where if a shake is a high enough frequency, show is called too quickly and an IllegalStateException is thrown.

I can mitigate this on my end using a high-pass filter on the accelerometer data - but preferably we'd either debounce show or get an accurate read on whether the fragment is currently added.

I've tried overriding the show method and using the transactional interface (which is used under the hood of DialogFragment's show method anyway, but I tried to reconfigure it to replace the fragment anyway), but the error is still thrown.

Any ideas?

miguelandres commented 7 years ago

Github said email replies don't support markdown... soooo

Thanks for taking the time to look into this Nick!

The sad part is that there is a method on FragmentTransaction that does exactly what we need, commitNow, but it's on API 24+, we want to support 15+, so that's out of the question.

The other thing I can think of, which is a tad (very) hacky is to use a isAddingFragment boolean (with all accesses guarded by a synchronized block) to keep track of one such instance, it would be set to true as soon as the show method is about to be called, and then set to false on one of the lifecycle methods (either onStart or onResume, not sure).

private Object syncLock = new Object();
private boolean isAddingFragment
public void showRemixer() {
  synchronized(syncLock) {
    if (!isAddingFragment) {
      isAddingFragment = true;
      show(.....);
    }
  }
}

public void onStart/onResume(...) {
  synchronized(syncLock) {
    isAddingFragment = false;
  }
}

Maybe even do it on onStop and call it "isFragmentAddedToStack"? dunno.

Let me know if you try something like this.

Thanks!!

nicksahler commented 7 years ago

Good timing - right before I submitted my pull request / gave up on show for the moment I started writing something very similar with synchronized blocks. I figured some sort of lock was ideal, but didn't complete it. I'll keep heading in this direction and update as I go.

nicksahler commented 7 years ago

onResume happens later in the fragment lifecycle I believe, so I am rolling with that. Pulling changes now.

nicksahler commented 7 years ago

Seems fixed! This should be closed, no?

I Think I tested it pretty thoroughly manually but would like to write actual tests (the reason being - I would also test sensor functionality).

I assume a new issue should be opened for that.

miguelandres commented 7 years ago

Let's repurpose this bug for that purpose, @nicksahler, that way we keep the context.