-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Audio playback refactor #3229
Audio playback refactor #3229
Conversation
99eac7b
to
5f86673
Compare
Interesting note from something I've realised: the way that swiping through pages/groups works in Collect means that it's hard for us to use the EDIT: A way nicer way to handle this is to just have the |
Our "nicer way" in my last comment only solves part of the problem as we still need a valid lifecycle for
|
Ok here's my findings from the spikes: 1. Give the ODKView it's own "lifecycle" using the Lifecycle tools Jetpack gives us. In the // ObserverToken
public class ObserverToken implements LifecycleOwner {
private LifecycleRegistry lifecycleRegistry = new LifecycleRegistry(this);
public ObserverToken() {
lifecycleRegistry.handleLifecycleEvent(ON_RESUME);
}
public void release() {
lifecycleRegistry.handleLifecycleEvent(ON_DESTROY);
}
@NonNull
@Override
public Lifecycle getLifecycle() {
return lifecycleRegistry;
}
}
// FormEntryActivity
private ObserverToken odkViewObserverToken = new ObserverToken();
public ObserverToken getODKViewObserverToken() {
return odkViewObserverToken;
}
private void releaseOdkView() {
odkViewObserverToken.release();
odkViewObserverToken = new ObserverToken();
if (odkView != null) {
odkView.releaseWidgetResources();
odkView = null;
}
} You can see here that we end up with the LifecycleOwner lifecycleOwner = activity.getODKViewObserverToken();
liveData.observe(lifeCycleOwner, () -> ...); 2. Remove lifecycle from observations and manage deregistering ourselves. Here I ended up with a // LiveDataObserver
public class LiveDataObserver {
private final List<Pair<LiveData, Observer>> observations = new ArrayList<>();
public <T> void observe(LiveData<T> liveData, Observer<T> observer) {
liveData.observeForever(observer);
observations.add(new Pair<>(liveData, observer));
}
public void release() {
for (Pair<LiveData, Observer> observation : observations) {
LiveData liveData = observation.first;
Observer observer = observation.second;
liveData.removeObserver(observer);
}
observations.clear();
}
}
/// MediaLayout (and anywhere we observer LiveData)
private LiveDataObserver liveDataObserver = new LiveDataObserver();
liveDataObserver.observe(liveData, () -> ...);
public void release() {
liveDataObserver.release();
} Here you can see that we need to be able to call a method on any view that uses LiveData 3. Make the ODKView a Fragment so it has lifecycle we can use. It may not surprising to hear this but: this seems like it's not going to be easy. Most likely we'd need to go for a full ViewPager/Fragment switch over. This would involve moving widget rendering code into a Fragment. I spent a bit of time on playing around with this but pulled out when it looked like it was going to end up being the rest of my week. RecommendationOption 2 leaves us with the ability to use We could of course drop It'd be great to get your thoughts on this @lognaturel and @grzesiek2010! |
Thanks for the clear presentation of your spike results, @seadowg. I'm convinced that if we're going to go in on Forgive me if this should be obvious, but I'm not clear on why you need to add the |
@lognaturel Yeah that would totally work. My thinking when I was spiking things out was just keeping things as contained as possible so it would be easier to present them back - that's how I ended up with the token rather than it in the view code. Moving forward though it might be a nice way to actually implement it as it keeps all the Jetpack lifecycle code out of the view itself which makes it feel a little more "bolt on" (and hopefully "bolt off"). What do you think? |
Ah yes, that did make for a really nice summary. 👍 I'm generally very much for compartmentalization but in this case, I wonder whether having the I don't feel super strongly about it but I am liking what I see in 6f1044a. It feels a little funky to have something that's a LifecycleOwner be called |
@lognaturel yeah I think I agree. On second thoughts I think this might also fit in to the |
@lognaturel ach I'd forgotten (weekend brain 🤷♂️) that it's awkward to have the lifecycle on the |
9722694
to
b4db32a
Compare
@seadowg and I have talked through this a couple of times and I'm on board with the current direction. The big thing I'd like to see in a cleanup pass is more narration (is anyone surprised). For example, capturing some of the back and forth we had above about where the lifecycle used for the I'm going to be out for the next two weeks. @grzesiek2010, please take over review! Don't hesitate to get on a call to discuss. I think it'd be good for this to go into QA as soon as possible because the big risk is that there's functionality missing. @seadowg says he'll likely have something ready for that mid-week. 🙏 |
This prevents problems where two buttons use the same audio file.
@mmarciniak90 sorry I forgot to readd the "needs-testing" label to this. Will do. |
@seadowg, the first thing that I noticed is that audio is not always stopped when I swipe to the next page. It works for |
@mmarciniak90 ah I think I see what might be wrong. Is the proble happening on the last screen of the form every time? |
@mmarciniak90 I've pushed a fix that should hopefully resolve that problem |
@seadowg sorry that I'm adding issues partially. I'm still working on testing this PR.
I also noticed behavior which could be improved but they exist on v1.23.3 and they are related only to Android 8.1 and 9.0
|
@mmarciniak90 no that's great. Good to get these things early! I'll have a look at the crash.
That was the old behaviour (I thought anyway) so I left it intact. The behaviour makes sense to me but maybe we play around with it in future to see if it's what people want?
Ah yeah that will work now. Do we think it should stop on rotate?
I saw this as well. It seemed on my devices (and emulator) Android was set to "loop video" (it's in the options menu of the video) by default. |
Sounds good
As I said, the last two points are visible in v1.23.3. |
Both of these feel like they're ok but I'll take a look into why they happen to make sure neither are suggesting some underlying problem. |
@lognaturel would be interesting to get your opinion of the style of error handling (using |
These kinds of errors could happen multiple times while a specific question/field list is being displayed so modeling them as events that are streamed seems fine to me. Any alternative I can think of would require the view model or audio helper to have some knowledge of where errors should be displayed. I can't think of an alternative using Where I haven't been able to find an analogous structure I liked is when the info that needs to be displayed is only displayed exactly once. For example, that's the case with the background location tracking warning toast and that's why I ultimately abandoned using a |
@lognaturel the only alternative I considered was having
Ah that's interesting. In my head you'd do this by only "emitting" the error once from a |
@mmarciniak90 I spent some time look into the orientation changes causing the audio to stop. I found the same thing that on API 26+ (8.0) the audio doesn't stop when the phone is rotated but on older devices it does. TLDR: this is because on devices before API 26 the app does the same thing when it rotates as when we change screens or switch apps and we stop audio at those points. Full(er) explanation: This seems to be because of a behaviour change in Android but is also to do with some potential weirdness on our side. It looks like we're using a flag in our Frankly this was really confusing and it took me a while to realize those flags were on the manifest. I couldn't find anything on the behaviour of |
@seadowg Thanks for the so precise analyze. This knowledge will be helpful for testing other PRs too. Tested with success Verified on Android: 4.2, 4.4, 5.1, 6.0, 7.0, 8.1, 9.0 Verified cases:
@opendatakit-bot unlabel "needs testing" |
@lognaturel @grzesiek2010 feel happy with this merging in? |
Thrilled! 🎉 |
Closes #2521
There's already been some good work and discussion around how to fix some of the bugs and quirks around audio playback in the app (#2522, #2863 and #2817 for example). For people not familiar with the bugs: there are multiple problems with play/stop buttons interacting with one another. After having a look at the problem, it made sense to give it another stab now that we have some new tooling maturing in the Android Jetpack world.
After digging into the current architecture it made sense to me that a good way forward would be to separate out audio playback functionality entirely from the question world so that it could be tested and implemented in isolation. To help myself out I ended up drawing a picture of this in Miro:
This draft starts with an initial pass at that that uses an
AudioPlayerViewModel
to coordinate theMediaPlayer
and expose state and actions to anAudioButton
view that is only responsible for rendering whether it is in the "playing" state or not and for emitting (via a listener) "playClicked" and "stopClicked" events.I worked through this with @lognaturel and one thing we noticed was how many different behaviors of the app that audio playback is part of. Although this isn't finished yet it made sense to get a draft PR up early to get feedback and also have people point out features we might be missing. I'm going to enumerate them below (and tick off what is working in this reimplementation) - shout out if anything is missing:
MediaPlayer
objects are released whenActivity
is destroyedMediaPlayer
objects are released whenActivity
is pausedAudioWidget
(and vice versa)Just to note: this PR does not fix the problems outlined in #2861 - autoplay for select options is not functioning correctly. We'll follow up with a fix for that once this is in.
What has been done to verify that this works as intended?
Used several different forms involving
AudioWidget
s, audio in question media and in select options etc etc.How does this change affect users? Describe intentional changes to behavior and behavior that could have accidentally been affected by code changes. In other words, what are the regression risks?
There might be some accidental changes to behavior given how little of this functionality was tested or documented. It would be to double check that media in forms generally still works as expected (I've listed the behaviours I could find above) and that we don't see any (negative) performance changes compared to the current version of the app. This is a big set of changes so I wouldn't be surprised if find problems - I'd imagine with something I didn't realize the app could do.
Do we need any specific form for testing your changes? If so, please attach one.
Good to use as many media forms as possible but a good one to start with is the "MediaForm" linked in #2521.
Does this change require updates to documentation? If so, please file an issue here and include the link below.
I think it would be good to add documentation for the audio functionality. I'll make sure to file an an issue once we're happy with this!
Before submitting this PR, please make sure you have:
./gradlew checkAll
and confirmed all checks still pass OR confirm CircleCI build passes and run./gradlew connectedDebugAndroidTest
locally.