Skip to content
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

Fix #123 & #116: Functional AudioPlayerController #133

Closed
wants to merge 10 commits into from

Conversation

jamesxu0
Copy link
Contributor

@jamesxu0 jamesxu0 commented Sep 18, 2019

Explanation

I didn't separate #123 & #116 because I already finished basic functionality for the AudioPlayerController.

Checklist

  • The PR title starts with "Fix #bugnum: ", followed by a short, clear summary of the changes. (If this PR fixes part of an issue, prefix the title with "Fix part of #bugnum: ...".)
  • The PR explanation includes the words "Fixes #bugnum: ..." (or "Fixes part of #bugnum" if the PR only partially fixes an issue).
  • The PR follows the style guide.
  • The PR does not contain any unnecessary auto-generated code from Android Studio.
  • The PR is made from a branch that's not called "develop".
  • The PR is assigned to an appropriate reviewer.

@jamesxu0
Copy link
Contributor Author

jamesxu0 commented Sep 18, 2019

I tried creating a test based on UserAppHistoryControllerTest, but I can't seem to run either. I get a compiler error "Dagger/MissingBinding" @org.oppia.util.logging.EnableConsoleLog java.lang.Boolean cannot be provided without an @Provides-annotated method". I know the LoggerModule has a provides so I'm not sure what's wrong. Also if you could suggest what kind of things I should be testing for, that would be great.

import org.robolectric.annotation.Config
import javax.inject.Singleton

/** Tests for [UserAppHistoryController]. */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update doc string to correspond to the correct component being tested here.


@Test
fun test() {
//TODO
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest starting with the easiest happy path case (usually not errors) to get a test to work. After that, you can copy & paste that to test other cases to be considered.

For this, I suggest starting with: testAudioPlayer_successfulInitialize_reportsSuccessfulInit() sets up media player so that the init will succeed, initializes the controller, and checks that the report LiveData is correct.

See http://robolectric.org/javadoc/4.0/org/robolectric/shadows/ShadowMediaPlayer.html for how to fake the media player. I think you'll need to do use ShadowMediaPlayer.setCreateListener to get access to the ShadowMediaPlayer corresponding to the instance of MediaPlayer you're testing. See http://robolectric.org/extending/ for a background on Shadows.


class AudioPlayerController @Inject constructor(val context: Context) {

class PlayStatus(val type: String, val value: Int)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might an enum be a better fit for the play status? Strings can be difficult to use since they aren't strongly typed, meaning if they change in one places other places will break at runtime rather than compile-time.

What purposes is the value? Could 'PlayStatus' just become an enum to represent the different states?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or, maybe a data object for an enum on status and other properties that should be passed to the UI could be a good fit (e.g. for duration, position, etc.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed in meeting: since LiveData observers can miss updates (e.g. during configuration changes), we need to model this based on absolute state. I think that means we need to include position & duration each time.

import java.util.concurrent.TimeUnit
import javax.inject.Inject

class AudioPlayerController @Inject constructor(val context: Context) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add documentation for this controller. It doesn't need to be super detailed, but consider what sort of information you would like to know in order to understand how to use this controller correctly if you came across this code and knew nothing about playing audio in the Oppia app. How to initialize, play, deinitialize, and caveats would be good things to include.

private var prepared = false
private val playState = MutableLiveData<PlayStatus>()

fun initializeMediaPlayer (stringUri: String) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: no space between function name and parameter '(' per https://google.github.io/styleguide/javaguide.html#s4.6.2-horizontal-whitespace.

private val playState = MutableLiveData<PlayStatus>()

fun initializeMediaPlayer (stringUri: String) {
if (mediaPlayer == null) mediaPlayer = MediaPlayer()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest we instead lazy initialize media player. Kotlin solves this exact pattern for us:

private val mediaPlayer: MediaPlayer by lazy { MediaPlayer() }

private var prepared = false
private val playState = MutableLiveData<PlayStatus>()

fun initializeMediaPlayer (stringUri: String) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have an initialize, we probably also need a deinitialize. Can you add a method for that?


private fun startUpdatingSeekBar() {
if (executor == null) executor = Executors.newSingleThreadScheduledExecutor()
executor?.scheduleAtFixedRate({ updateSeekBar() }, 0, 1000, TimeUnit.MILLISECONDS)
Copy link
Member

@BenHenning BenHenning Sep 18, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who removes this schedule? Does MediaPlayer not provide a way for us to monitor the progress?

If not, we should use the background Kotlin dispatcher used by the DataProviders object. Unfortunately there isn't as nice of an API for scheduling at a fixed rate in Kotlin, but with delay() you can get close (and get some additional benefits I'll mention later):

fun scheduleNextSeekBarUpdate() {
  nextUpdateJob = backgroundDispatcherScope.launch {
    delay(TimeUnit.SECONDS.toMillis(1)) // TODO: move delay to constant
    updateSeekBar()
    scheduleNextSeekBarUpdate()
  }
  // NB: nextUpdateJob will need to be locked since it can be changed on multiple threads
}

Then, you can cancel the nextUpdateJob both during completion (like you similarly do with the executor service in stopUpdatingSeekBar below) and during deinitialization (which presumably is called by the UI when the user navigates away from the activity).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From discussion & following up: I think we actually want a custom MutableLiveData class to schedule/stop in onActive/onInactive to ensure that we're only updating at times that observers actually care about it.

@BenHenning BenHenning removed their assignment Sep 18, 2019

private fun startUpdatingSeekBar() {
if (executor == null) executor = Executors.newSingleThreadScheduledExecutor()
executor?.scheduleAtFixedRate({ updateSeekBar() }, 0, 1000, TimeUnit.MILLISECONDS)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From discussion & following up: I think we actually want a custom MutableLiveData class to schedule/stop in onActive/onInactive to ensure that we're only updating at times that observers actually care about it.


class AudioPlayerController @Inject constructor(val context: Context) {

class PlayStatus(val type: String, val value: Int)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed in meeting: since LiveData observers can miss updates (e.g. during configuration changes), we need to model this based on absolute state. I think that means we need to include position & duration each time.

@BenHenning
Copy link
Member

Regarding your error, I accidentally forgot some of the test-only bindings in the test you referenced, but they're there now: https://github.com/oppia/oppia-android/blob/develop/domain/src/test/java/org/oppia/domain/UserAppHistoryControllerTest.kt#L209. You can copy these into your test and it should work.

@jamesxu0
Copy link
Contributor Author

Started a new pull request #149 to make getting changes in from develop easier

@jamesxu0 jamesxu0 closed this Sep 19, 2019
@BenHenning BenHenning deleted the audio-player branch June 10, 2020 22:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants