Niedzielski has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/250903

Change subject: Add pronounciation media player logic
......................................................................

Add pronounciation media player logic

Add logic for playing media. The initial network request will come in a
later patch.

Bug: T114524
Change-Id: I4cf7b0be940085aad1a6931d5eee3215de80488c
---
M app/src/main/java/org/wikipedia/media/AvPlayer.java
A app/src/main/java/org/wikipedia/media/AvPlayerImplementation.java
M app/src/main/java/org/wikipedia/media/DefaultAvPlayer.java
A app/src/main/java/org/wikipedia/media/MediaPlayerImplementation.java
A app/src/main/java/org/wikipedia/media/State.java
M app/src/main/java/org/wikipedia/richtext/AudioUrlSpan.java
M app/src/main/java/org/wikipedia/richtext/TextViewSpanOnTouchListener.java
M app/src/main/java/org/wikipedia/views/ArticleHeaderView.java
A app/src/test/java/org/wikipedia/media/DefaultAvPlayerTest.java
A app/src/test/java/org/wikipedia/media/FakeAvPlayerImplementation.java
A app/src/test/java/org/wikipedia/media/StateTest.java
11 files changed, 1,065 insertions(+), 37 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/apps/android/wikipedia 
refs/changes/03/250903/1

diff --git a/app/src/main/java/org/wikipedia/media/AvPlayer.java 
b/app/src/main/java/org/wikipedia/media/AvPlayer.java
index 01107d1..0acab89 100644
--- a/app/src/main/java/org/wikipedia/media/AvPlayer.java
+++ b/app/src/main/java/org/wikipedia/media/AvPlayer.java
@@ -1,11 +1,29 @@
 package org.wikipedia.media;
 
+import android.support.annotation.NonNull;
+
 public interface AvPlayer {
-    void init();
+    interface Callback {
+        void onSuccess();
+    }
+
+    interface ErrorCallback {
+        void onError();
+    }
+
     void deinit();
-    void load(String path);
-    void play();
-    void togglePlayback();
+    void init();
+
+    void load(@NonNull String path,
+              @NonNull Callback callback,
+              @NonNull ErrorCallback errorCallback);
+
     void stop();
-    void seek(int millis);
+
+    void play(@NonNull Callback callback, @NonNull ErrorCallback 
errorCallback);
+    void play(@NonNull String path,
+              @NonNull Callback callback,
+              @NonNull ErrorCallback errorCallback);
+
+    void pause();
 }
\ No newline at end of file
diff --git a/app/src/main/java/org/wikipedia/media/AvPlayerImplementation.java 
b/app/src/main/java/org/wikipedia/media/AvPlayerImplementation.java
new file mode 100644
index 0000000..8348d7d
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/media/AvPlayerImplementation.java
@@ -0,0 +1,14 @@
+package org.wikipedia.media;
+
+import android.support.annotation.NonNull;
+
+interface AvPlayerImplementation {
+    void deinit();
+    void init();
+    void load(@NonNull String path,
+              @NonNull AvPlayer.Callback callback,
+              @NonNull AvPlayer.ErrorCallback errorCallback);
+    void stop();
+    void play(@NonNull AvPlayer.Callback callback, @NonNull 
AvPlayer.ErrorCallback errorCallback);
+    void pause();
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/wikipedia/media/DefaultAvPlayer.java 
b/app/src/main/java/org/wikipedia/media/DefaultAvPlayer.java
index 5514a53..770d78d 100644
--- a/app/src/main/java/org/wikipedia/media/DefaultAvPlayer.java
+++ b/app/src/main/java/org/wikipedia/media/DefaultAvPlayer.java
@@ -1,36 +1,119 @@
 package org.wikipedia.media;
 
+import android.support.annotation.NonNull;
+
 public class DefaultAvPlayer implements AvPlayer {
-    @Override
-    public void init() {
+    @NonNull
+    private final AvPlayerImplementation player;
+    @NonNull
+    private final State state = new State();
+
+    public DefaultAvPlayer(@NonNull AvPlayerImplementation player) {
+        this.player = player;
     }
 
     @Override
     public void deinit() {
+        if (state.isInit()) {
+            player.deinit();
+            state.setDeinit();
+        }
     }
 
     @Override
-    public void load(String path) {
-
+    public void init() {
+        if (state.isDeinit()) {
+            player.init();
+            state.setInit();
+        }
     }
 
     @Override
-    public void play() {
-
-    }
-
-    @Override
-    public void togglePlayback() {
-
+    public void load(@NonNull String path,
+                     @NonNull final Callback callback,
+                     @NonNull final ErrorCallback errorCallback) {
+        init();
+        if (!state.isLoading(path) && !state.isLoaded(path)) {
+            state.setLoading(path);
+            player.load(path, new Callback() {
+                @Override
+                public void onSuccess() {
+                    state.setLoaded();
+                    if (state.isPlaying()) {
+                        player.play(new StopCallbackWrapper(callback), new 
StopErrorCallbackWrapper(errorCallback));
+                    } else {
+                        callback.onSuccess();
+                    }
+                }
+            }, new ErrorCallback() {
+                @Override
+                public void onError() {
+                    state.setInit();
+                    errorCallback.onError();
+                }
+            });
+        }
     }
 
     @Override
     public void stop() {
-
+        if (state.isLoaded() && !state.isStopped()) {
+            player.stop();
+        }
+        state.setStopped();
     }
 
     @Override
-    public void seek(int millis) {
-
+    public void play(@NonNull Callback callback, @NonNull ErrorCallback 
errorCallback) {
+        if (state.isLoaded() && !state.isPlaying()) {
+            state.setPlaying();
+            player.play(new StopCallbackWrapper(callback), new 
StopErrorCallbackWrapper(errorCallback));
+        } else {
+            state.setPlaying();
+        }
     }
-}
+
+    @Override
+    public void play(@NonNull String path,
+                     @NonNull Callback callback,
+                     @NonNull ErrorCallback errorCallback) {
+        load(path, callback, errorCallback);
+        play(callback, errorCallback);
+    }
+
+    @Override
+    public void pause() {
+        if (state.isLoaded() && state.isPlaying()) {
+            player.pause();
+        }
+        state.setPaused();
+    }
+
+    private class StopCallbackWrapper implements Callback {
+        @NonNull private final Callback callback;
+
+        StopCallbackWrapper(@NonNull Callback callback) {
+            this.callback = callback;
+        }
+
+        @Override
+        public void onSuccess() {
+            state.setStopped();
+            callback.onSuccess();
+        }
+    }
+
+    private class StopErrorCallbackWrapper implements ErrorCallback {
+        @NonNull private final ErrorCallback errorCallback;
+
+        StopErrorCallbackWrapper(@NonNull ErrorCallback errorCallback) {
+            this.errorCallback = errorCallback;
+        }
+
+        @Override
+        public void onError() {
+            state.setStopped();
+            errorCallback.onError();
+        }
+    }
+}
\ No newline at end of file
diff --git 
a/app/src/main/java/org/wikipedia/media/MediaPlayerImplementation.java 
b/app/src/main/java/org/wikipedia/media/MediaPlayerImplementation.java
new file mode 100644
index 0000000..670e906
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/media/MediaPlayerImplementation.java
@@ -0,0 +1,177 @@
+package org.wikipedia.media;
+
+import android.media.MediaPlayer;
+import android.support.annotation.NonNull;
+
+import org.wikipedia.util.log.L;
+
+import java.io.IOException;
+
+public class MediaPlayerImplementation implements AvPlayerImplementation {
+    private static final boolean VERBOSE = false;
+    private boolean paused;
+
+    @NonNull private final MediaPlayer player = new MediaPlayer();
+
+    @Override
+    public void deinit() {
+        if (VERBOSE) {
+            L.v("Releasing");
+        }
+        player.release();
+    }
+
+    @Override
+    public void init() {
+        if (VERBOSE) {
+            L.v("Resetting");
+        }
+        player.reset();
+    }
+
+    @Override
+    public void load(@NonNull String path,
+                     @NonNull AvPlayer.Callback callback,
+                     @NonNull AvPlayer.ErrorCallback errorCallback) {
+        load(path, new PreparedListenerCallbackWrapper(callback),
+                new ErrorListenerErrorCallbackWrapper(errorCallback));
+    }
+
+    @Override
+    public void stop() {
+        if (VERBOSE) {
+            L.v("Stopping");
+        }
+
+        // Do not call MediaPlayer.stop(). This requires going through the 
whole lifecycle again.
+        if (!paused) {
+            pause();
+        }
+        player.seekTo(0);
+    }
+
+    @Override
+    public void play(@NonNull AvPlayer.Callback callback,
+                     @NonNull AvPlayer.ErrorCallback errorCallback) {
+        play(new CompletionListenerCallbackWrapper(callback),
+                new ErrorListenerErrorCallbackWrapper(errorCallback));
+    }
+
+    @Override
+    public void pause() {
+        if (VERBOSE) {
+            L.v("Pausing");
+        }
+        paused = true;
+        player.pause();
+    }
+
+    private void load(@NonNull String path,
+                      @NonNull MediaPlayer.OnPreparedListener listener,
+                      @NonNull MediaPlayer.OnErrorListener errorListener) {
+        if (VERBOSE) {
+            L.v("Loading");
+        }
+        paused = false;
+        player.setOnPreparedListener(listener);
+        player.setOnErrorListener(errorListener);
+        if (setDataSource(path)) {
+            player.prepareAsync();
+        } else {
+            errorListener.onError(player, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+        }
+    }
+
+    private void play(@NonNull MediaPlayer.OnCompletionListener listener,
+                      @NonNull MediaPlayer.OnErrorListener errorListener) {
+        if (VERBOSE) {
+            L.v("Playing");
+        }
+        paused = false;
+        player.setOnCompletionListener(listener);
+        player.setOnErrorListener(errorListener);
+        player.start();
+    }
+
+    private boolean setDataSource(@NonNull String path) {
+        try {
+            player.setDataSource(path);
+            return true;
+        } catch (IOException e) {
+            L.d(e);
+            return false;
+        }
+    }
+
+    private abstract static class CallbackWrapper {
+        @NonNull private final AvPlayer.Callback callback;
+        CallbackWrapper(@NonNull AvPlayer.Callback callback) {
+            this.callback =  callback;
+        }
+
+        protected void onSuccess() {
+            callback.onSuccess();
+        }
+    }
+
+    private abstract static class ErrorCallbackWrapper {
+        @NonNull private final AvPlayer.ErrorCallback errorCallback;
+        ErrorCallbackWrapper(@NonNull AvPlayer.ErrorCallback errorCallback) {
+            this.errorCallback =  errorCallback;
+        }
+
+        protected void onError() {
+            errorCallback.onError();
+        }
+    }
+
+    private static class PreparedListenerCallbackWrapper extends 
CallbackWrapper implements
+            MediaPlayer.OnPreparedListener {
+        PreparedListenerCallbackWrapper(@NonNull AvPlayer.Callback callback) {
+            super(callback);
+        }
+
+        @Override
+        public void onPrepared(MediaPlayer player) {
+            if (VERBOSE) {
+                L.v("Loaded");
+            }
+            onSuccess();
+        }
+    }
+
+    private static class CompletionListenerCallbackWrapper extends 
CallbackWrapper implements
+            MediaPlayer.OnCompletionListener {
+        CompletionListenerCallbackWrapper(@NonNull AvPlayer.Callback callback) 
{
+            super(callback);
+        }
+
+        @Override
+        public void onCompletion(MediaPlayer mp) {
+            if (VERBOSE) {
+                L.v("Stopped");
+            }
+            onSuccess();
+        }
+    }
+
+    private static class ErrorListenerErrorCallbackWrapper extends 
ErrorCallbackWrapper implements
+            MediaPlayer.OnErrorListener {
+        ErrorListenerErrorCallbackWrapper(@NonNull AvPlayer.ErrorCallback 
errorCallback) {
+            super(errorCallback);
+        }
+
+        @Override
+        public boolean onError(MediaPlayer mp, int what, int extra) {
+            if (VERBOSE) {
+                L.v("Error: what=" + formatHex(what) + " extra=" + 
formatHex(extra));
+            }
+            onError();
+            return true;
+        }
+
+        private String formatHex(int hex) {
+            return String.format("x%08x", hex);
+        }
+    }
+}
diff --git a/app/src/main/java/org/wikipedia/media/State.java 
b/app/src/main/java/org/wikipedia/media/State.java
new file mode 100644
index 0000000..4770aa5
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/media/State.java
@@ -0,0 +1,100 @@
+package org.wikipedia.media;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import org.wikipedia.util.StringUtil;
+
+class State {
+    private enum LoadState {
+        DEINIT, INIT, LOADING, LOADED
+    }
+
+    private enum PlayState {
+        STOPPED, PLAYING, PAUSED
+    }
+
+    @NonNull
+    private LoadState loadState = LoadState.DEINIT;
+    @NonNull
+    private PlayState playState = PlayState.STOPPED;
+    @Nullable
+    private String path;
+
+    @Nullable
+    public String getPath() {
+        return path;
+    }
+
+    public boolean isDeinit() {
+        return loadState == LoadState.DEINIT;
+    }
+
+    public void setDeinit() {
+        loadState = LoadState.DEINIT;
+        playState = PlayState.STOPPED;
+    }
+
+    public boolean isInit() {
+        return !isDeinit();
+    }
+
+    public void setInit() {
+        loadState = LoadState.INIT;
+    }
+
+    public boolean isLoading() {
+        return loadState == LoadState.LOADING;
+    }
+
+    public boolean isLoading(@Nullable String path) {
+        return isLoading() && isPathIdentical(path);
+    }
+
+    public void setLoading(@Nullable String path) {
+        if (!isLoaded(path)) {
+            loadState = LoadState.LOADING;
+            this.path = path;
+        }
+    }
+
+    public boolean isLoaded() {
+        return loadState == LoadState.LOADED;
+    }
+
+    public boolean isLoaded(@Nullable String path) {
+        return isLoaded() && isPathIdentical(path);
+    }
+
+    public void setLoaded() {
+        loadState = LoadState.LOADED;
+    }
+
+    public boolean isStopped() {
+        return playState == PlayState.STOPPED;
+    }
+
+    public void setStopped() {
+        playState = PlayState.STOPPED;
+    }
+
+    public boolean isPlaying() {
+        return playState == PlayState.PLAYING;
+    }
+
+    public void setPlaying() {
+        playState = PlayState.PLAYING;
+    }
+
+    public boolean isPaused() {
+        return playState == PlayState.PAUSED;
+    }
+
+    public void setPaused() {
+        playState = PlayState.PAUSED;
+    }
+
+    private boolean isPathIdentical(@Nullable String path) {
+        return StringUtil.equals(this.path, path);
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/wikipedia/richtext/AudioUrlSpan.java 
b/app/src/main/java/org/wikipedia/richtext/AudioUrlSpan.java
index 90fb13b..21a84a9 100644
--- a/app/src/main/java/org/wikipedia/richtext/AudioUrlSpan.java
+++ b/app/src/main/java/org/wikipedia/richtext/AudioUrlSpan.java
@@ -24,10 +24,16 @@
     @NonNull
     private final AvPlayer player;
 
-    public AudioUrlSpan(@NonNull View view, @NonNull AvPlayer player) {
+    @NonNull
+    private final AvCallback avCallback = new AvCallback();
+
+    @NonNull
+    private final String path;
+
+    public AudioUrlSpan(@NonNull View view, @NonNull AvPlayer player, @NonNull 
String path) {
         super(view, drawable(view.getContext()));
         this.player = player;
-        view.addOnAttachStateChangeListener(new ViewAttachListener());
+        this.path = path;
     }
 
     public void setTint(@ColorInt int color) {
@@ -43,7 +49,7 @@
     public void start() {
         showIcon(PLAY_ICON_LEVEL);
         super.start();
-        player.play();
+        player.play(path, avCallback, avCallback);
     }
 
     @Override
@@ -51,12 +57,6 @@
         showIcon(STOP_ICON_LEVEL);
         super.stop();
         player.stop();
-    }
-
-    @Override
-    public void toggle() {
-        super.toggle();
-        player.togglePlayback();
     }
 
     @Override
@@ -104,15 +104,16 @@
         return context.getResources().getDimensionPixelSize(id);
     }
 
-    private class ViewAttachListener implements 
View.OnAttachStateChangeListener {
+    private class AvCallback implements AvPlayer.Callback, 
AvPlayer.ErrorCallback {
         @Override
-        public void onViewAttachedToWindow(View view) {
-            player.init();
+        public void onSuccess() {
+            stop();
         }
 
+
         @Override
-        public void onViewDetachedFromWindow(View v) {
-            player.deinit();
+        public void onError() {
+            stop();
         }
     }
 }
\ No newline at end of file
diff --git 
a/app/src/main/java/org/wikipedia/richtext/TextViewSpanOnTouchListener.java 
b/app/src/main/java/org/wikipedia/richtext/TextViewSpanOnTouchListener.java
index 01f9edc..e40604f 100644
--- a/app/src/main/java/org/wikipedia/richtext/TextViewSpanOnTouchListener.java
+++ b/app/src/main/java/org/wikipedia/richtext/TextViewSpanOnTouchListener.java
@@ -10,7 +10,7 @@
 import android.widget.TextView;
 
 public class TextViewSpanOnTouchListener implements View.OnTouchListener {
-    @Nullable private TextView textView;
+    @NonNull private TextView textView;
 
     public TextViewSpanOnTouchListener(@NonNull TextView textView) {
         this.textView = textView;
diff --git a/app/src/main/java/org/wikipedia/views/ArticleHeaderView.java 
b/app/src/main/java/org/wikipedia/views/ArticleHeaderView.java
index 031836a..69168cd 100644
--- a/app/src/main/java/org/wikipedia/views/ArticleHeaderView.java
+++ b/app/src/main/java/org/wikipedia/views/ArticleHeaderView.java
@@ -32,7 +32,9 @@
 import org.wikipedia.R;
 import org.wikipedia.Utils;
 import org.wikipedia.WikipediaApp;
+import org.wikipedia.media.AvPlayer;
 import org.wikipedia.media.DefaultAvPlayer;
+import org.wikipedia.media.MediaPlayerImplementation;
 import org.wikipedia.page.leadimages.ImageViewWithFace;
 import org.wikipedia.page.leadimages.ImageViewWithFace.OnImageLoadListener;
 import org.wikipedia.richtext.LeadingSpan;
@@ -54,6 +56,8 @@
     @NonNull private CharSequence title = "";
     @NonNull private CharSequence subtitle = "";
     @Nullable private String pronunciationUrl;
+
+    @NonNull private final AvPlayer avPlayer = new DefaultAvPlayer(new 
MediaPlayerImplementation());
 
     public ArticleHeaderView(Context context) {
         super(context);
@@ -193,7 +197,21 @@
         return pronunciationUrl != null;
     }
 
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        avPlayer.init();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        avPlayer.deinit();
+    }
+
     private void updateText() {
+        avPlayer.stop();
+
         SpannableStringBuilder builder = new SpannableStringBuilder(title);
 
         if (hasPronunciation()) {
@@ -210,7 +228,7 @@
     }
 
     private Spanned pronunciationSpanned() {
-        AudioUrlSpan span = new AudioUrlSpan(text, new DefaultAvPlayer());
+        AudioUrlSpan span = new AudioUrlSpan(text, avPlayer, pronunciationUrl);
         span.setTint(hasImage() ? Color.WHITE : getContrastingThemeColor());
         return RichTextUtil.setSpans(new SpannableString(" "),
                 0,
@@ -267,7 +285,7 @@
 
     private void initText() {
         // TODO: replace with android:fontFamily="serif" attribute when our 
minimum API level is
-        //       Jelly Bean, API 16, or we make custom typeface attribute.
+        //       Jelly Bean, API 16, or if we make custom typeface attribute.
         text.setTypeface(Typeface.create(Typeface.SERIF, Typeface.NORMAL));
     }
 
diff --git a/app/src/test/java/org/wikipedia/media/DefaultAvPlayerTest.java 
b/app/src/test/java/org/wikipedia/media/DefaultAvPlayerTest.java
new file mode 100644
index 0000000..f3ae699
--- /dev/null
+++ b/app/src/test/java/org/wikipedia/media/DefaultAvPlayerTest.java
@@ -0,0 +1,226 @@
+package org.wikipedia.media;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.wikipedia.test.TestRunner;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+@RunWith(TestRunner.class)
+public class DefaultAvPlayerTest {
+    private static final String PATH_A = "http://pathA";;
+    private static final String PATH_B = "http://pathB";;
+
+    private final FakeAvPlayerImplementation implementation = spy(new 
FakeAvPlayerImplementation());
+    private AvPlayer subject = new DefaultAvPlayer(implementation);
+    private final AvPlayer.Callback callback = mock(AvPlayer.Callback.class);
+    private final AvPlayer.ErrorCallback errorCallback = 
mock(AvPlayer.ErrorCallback.class);
+
+    @Test
+    public void testDeinitDeinit() {
+        subject.deinit();
+        verify(implementation, never()).deinit();
+    }
+
+    @Test
+    public void testDeinitInit() {
+        subject.init();
+        subject.deinit();
+        verify(implementation).deinit();
+    }
+
+    @Test
+    public void testDeinitLoad() {
+        implementation.setAsyncLoadFailure(true);
+        load(PATH_A);
+        subject.deinit();
+        verify(implementation).deinit();
+    }
+
+    @Test
+    public void testInitConstructor() {
+        verify(implementation, never()).init();
+    }
+
+    @Test
+    public void testInitDeinit() {
+        subject.deinit();
+        subject.init();
+        verify(implementation).init();
+    }
+
+    @Test
+    public void testInitInit() {
+        subject.init();
+        subject.init();
+        verify(implementation).init();
+    }
+
+    @Test
+    public void testInitLoad() {
+        load(PATH_A);
+        verify(implementation).init();
+    }
+
+    @Test
+    public void testInitStop() {
+        subject.stop();
+        verify(implementation, never()).init();
+    }
+
+    @Test
+    public void testLoadLoad() {
+        load(PATH_A);
+        verifyLoad(1, PATH_A);
+        verifyCallback(1);
+        verifyErrorCallback(0);
+    }
+
+    @Test
+    public void testLoadReloadSync() {
+        load(PATH_A);
+        load(PATH_B);
+        verifyLoad(1, PATH_B);
+        verifyCallback(2);
+        verifyErrorCallback(0);
+    }
+
+    @Test
+    public void testLoadReloadAsync() {
+        implementation.setAsyncLoadFailure(true);
+        load(PATH_A);
+        implementation.setAsyncLoadFailure(false);
+        load(PATH_B);
+        verifyLoad(1, PATH_B);
+        verifyCallback(1);
+        verifyErrorCallback(0);
+    }
+
+    @Test
+    public void testLoadFail() {
+        implementation.setSyncLoadFailure(true);
+        load(PATH_A);
+        verifyLoad(1, PATH_A);
+        verifyCallback(0);
+        verifyErrorCallback(1);
+    }
+
+    @Test
+    public void testLoadPlay() {
+        play(PATH_A);
+        verifyLoad(1, PATH_A);
+        verifyCallback(1);
+        verifyErrorCallback(0);
+    }
+
+    @Test
+    public void testStopConstructor() {
+        subject.stop();
+        verify(implementation, never()).stop();
+    }
+
+    @Test
+    public void testStopLoad() {
+        implementation.setAsyncLoadFailure(true);
+        load(PATH_A);
+        subject.stop();
+        verify(implementation, never()).stop();
+        verifyCallback(0);
+        verifyErrorCallback(0);
+    }
+
+    @Test
+    public void testStopPlaySync() {
+        play(PATH_A);
+        subject.stop();
+        verify(implementation, never()).stop();
+        verifyCallback(1);
+        verifyErrorCallback(0);
+    }
+
+    @Test
+    public void testStopPlayAsync() {
+        implementation.setAsyncPlayFailure(true);
+        play(PATH_A);
+        subject.stop();
+        verify(implementation).stop();
+        verifyCallback(0);
+        verifyErrorCallback(0);
+    }
+
+    @Test
+    public void testPlayPlaySync() {
+        play(PATH_A);
+        play(PATH_A);
+        verifyPlay(2);
+        verifyCallback(2);
+        verifyErrorCallback(0);
+    }
+
+    @Test
+    public void testPlayPlaySyncFail() {
+        implementation.setSyncPlayFailure(true);
+        play(PATH_A);
+        verifyPlay(1);
+        verifyCallback(0);
+        verifyErrorCallback(1);
+    }
+
+    @Test
+    public void testPlayLoadSyncFail() {
+        implementation.setSyncLoadFailure(true);
+        play(PATH_A);
+        verifyPlay(0);
+        verifyCallback(0);
+        verifyErrorCallback(1);
+    }
+
+    @Test
+    public void testPlayPlayAsync() {
+        implementation.setAsyncPlayFailure(true);
+        play(PATH_A);
+        implementation.setAsyncPlayFailure(false);
+        play(PATH_A);
+        verifyPlay(1);
+        verifyCallback(0);
+        verifyErrorCallback(0);
+    }
+
+    private void load(String path) {
+        subject.load(path, callback, errorCallback);
+    }
+
+    private void play(String path) {
+        subject.play(path, callback, errorCallback);
+    }
+
+    private void verifyCallback(int times) {
+        verify(callback, times(times)).onSuccess();
+    }
+
+    private void verifyErrorCallback(int times) {
+        verify(errorCallback, times(times)).onError();
+    }
+
+    private void verifyLoad(int times, String path) {
+        verify(implementation, times(times)).load(eq(path), anyCallback(), 
anyErrorCallback());
+    }
+
+    private void verifyPlay(int times) {
+        verify(implementation, times(times)).play(anyCallback(), 
anyErrorCallback());
+    }
+
+    private AvPlayer.Callback anyCallback() {
+        return any(AvPlayer.Callback.class);
+    }
+
+    private AvPlayer.ErrorCallback anyErrorCallback() {
+        return any(AvPlayer.ErrorCallback.class);
+    }
+}
\ No newline at end of file
diff --git 
a/app/src/test/java/org/wikipedia/media/FakeAvPlayerImplementation.java 
b/app/src/test/java/org/wikipedia/media/FakeAvPlayerImplementation.java
new file mode 100644
index 0000000..34e1e59
--- /dev/null
+++ b/app/src/test/java/org/wikipedia/media/FakeAvPlayerImplementation.java
@@ -0,0 +1,59 @@
+package org.wikipedia.media;
+
+import android.support.annotation.NonNull;
+
+class FakeAvPlayerImplementation implements AvPlayerImplementation {
+    private boolean asyncLoadFailure;
+    private boolean syncLoadFailure;
+    private boolean asyncPlayFailure;
+    private boolean syncPlayFailure;
+
+    public void setAsyncLoadFailure(boolean enabled) {
+        asyncLoadFailure = enabled;
+    }
+
+    public void setSyncLoadFailure(boolean enabled) {
+        syncLoadFailure = enabled;
+    }
+
+    public void setAsyncPlayFailure(boolean enabled) {
+        asyncPlayFailure = enabled;
+    }
+
+    public void setSyncPlayFailure(boolean enabled) {
+        syncPlayFailure = enabled;
+    }
+
+    @Override public void deinit() { }
+
+    @Override public void init() { }
+
+    @Override
+    public void load(@NonNull String path,
+                     @NonNull AvPlayer.Callback callback,
+                     @NonNull AvPlayer.ErrorCallback errorCallback) {
+        if (asyncLoadFailure) {
+            // no callback
+        } else if (syncLoadFailure) {
+            errorCallback.onError();
+        } else {
+            callback.onSuccess();
+        }
+    }
+
+    @Override public void stop() { }
+
+    @Override
+    public void play(@NonNull AvPlayer.Callback callback,
+                     @NonNull AvPlayer.ErrorCallback errorCallback) {
+        if (asyncPlayFailure) {
+            // no callback
+        } else if (syncPlayFailure) {
+            errorCallback.onError();
+        } else {
+            callback.onSuccess();
+        }
+    }
+
+    @Override public void pause() { }
+}
\ No newline at end of file
diff --git a/app/src/test/java/org/wikipedia/media/StateTest.java 
b/app/src/test/java/org/wikipedia/media/StateTest.java
new file mode 100644
index 0000000..55d2fba
--- /dev/null
+++ b/app/src/test/java/org/wikipedia/media/StateTest.java
@@ -0,0 +1,332 @@
+package org.wikipedia.media;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.wikipedia.test.TestRunner;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+@RunWith(TestRunner.class)
+public class StateTest {
+    private static final String PATH_A = "http://pathA";;
+    private static final String PATH_B = "http://pathB";;
+
+    private State subject;
+
+    @Before
+    public void setUp() {
+        subject = new State();
+    }
+
+    @Test
+    public void testGetPathConstructor() {
+        assertThat(subject.getPath(), is((String) null));
+    }
+
+    @Test
+    public void testGetPathDeinit() {
+        subject.setLoading(PATH_A);
+        subject.setDeinit();
+        assertPath(PATH_A);
+    }
+
+    @Test
+    public void testGetPathInit() {
+        subject.setLoading(PATH_A);
+        subject.setInit();
+        assertPath(PATH_A);
+    }
+
+    @Test
+    public void testGetPathLoaded() {
+        subject.setLoading(PATH_A);
+        subject.setLoaded();
+        assertPath(PATH_A);
+    }
+
+    @Test
+    public void testIsDeinitConstructor() {
+        assertDeinit(true);
+    }
+
+    @Test
+    public void testIsDeinitInit() {
+        subject.setInit();
+        assertDeinit(false);
+    }
+
+    @Test
+    public void testSetDeinitInit() {
+        subject.setInit();
+        subject.setDeinit();
+        assertDeinit(true);
+    }
+
+    @Test
+    public void testIsInitConstructor() {
+        assertInit(false);
+    }
+
+    @Test
+    public void testIsInitDeinit() {
+        subject.setDeinit();
+        assertInit(false);
+    }
+
+    @Test
+    public void testIsInitInit() {
+        subject.setInit();
+        assertInit(true);
+    }
+
+    @Test
+    public void testIsInitLoading() {
+        subject.setLoading(PATH_A);
+        assertInit(true);
+    }
+
+    @Test
+    public void testIsInitLoaded() {
+        subject.setLoaded();
+        assertInit(true);
+    }
+
+    @Test
+    public void testSetInitDeinit() {
+        subject.setDeinit();
+        subject.setInit();
+        assertInit(true);
+    }
+
+    @Test
+    public void testIsLoadingConstructor() {
+        assertThat(subject.isLoading(), is(false));
+    }
+
+    @Test
+    public void testIsLoadingLoading() {
+        subject.setLoading(PATH_A);
+        assertThat(subject.isLoading(), is(true));
+    }
+
+    @Test
+    public void testIsLoadingLoaded() {
+        subject.setLoading(PATH_A);
+        subject.setLoaded();
+        assertThat(subject.isLoading(), is(false));
+    }
+
+    @Test
+    public void testIsLoadingLoadAgain() {
+        subject.setLoading(PATH_A);
+        subject.setLoaded();
+        subject.setLoading(PATH_B);
+        assertThat(subject.isLoading(), is(true));
+    }
+
+    @Test
+    public void testIsLoadingPathConstructor() {
+        assertThat(subject.isLoading(PATH_A), is(false));
+    }
+
+    @Test
+    public void testIsLoadingPathLoading() {
+        subject.setLoading(PATH_A);
+        assertThat(subject.isLoading(PATH_A), is(true));
+    }
+
+    @Test
+    public void testIsLoadingPathLoaded() {
+        subject.setLoading(PATH_A);
+        subject.setLoaded();
+        assertThat(subject.isLoading(PATH_A), is(false));
+    }
+
+    @Test
+    public void testIsLoadingPathLoadAgain() {
+        subject.setLoading(PATH_A);
+        subject.setLoaded();
+        subject.setLoading(PATH_B);
+        assertThat(subject.isLoading(PATH_B), is(true));
+    }
+
+    @Test
+    public void testIsLoadingPathLoadAgainPreviousPath() {
+        subject.setLoading(PATH_A);
+        subject.setLoading(PATH_B);
+        assertThat(subject.isLoading(PATH_A), is(false));
+    }
+
+    @Test
+    public void testSetLoadingInit() {
+        subject.setInit();
+        subject.setLoading(PATH_A);
+        assertThat(subject.isLoading(), is(true));
+    }
+
+    @Test
+    public void testIsLoadedConstructor() {
+        assertLoaded(false);
+    }
+
+    @Test
+    public void testIsLoadedLoading() {
+        subject.setLoading(PATH_A);
+        assertLoaded(false);
+    }
+
+    @Test
+    public void testIsLoadedLoaded() {
+        subject.setLoading(PATH_A);
+        subject.setLoaded();
+        assertLoaded(true);
+    }
+
+    @Test
+    public void testIsLoadedPathConstructor() {
+        assertLoaded(PATH_A, false);
+    }
+
+    @Test
+    public void testIsLoadedPathLoading() {
+        subject.setLoading(PATH_A);
+        assertLoaded(PATH_A, false);
+    }
+
+    @Test
+    public void testIsLoadedPathLoaded() {
+        subject.setLoading(PATH_A);
+        subject.setLoaded();
+        assertLoaded(PATH_A, true);
+    }
+
+    @Test
+    public void testIsLoadedPathLoadAgain() {
+        subject.setLoading(PATH_A);
+        subject.setLoading(PATH_B);
+        subject.setLoaded();
+        assertLoaded(PATH_B, true);
+    }
+
+    @Test
+    public void testIsLoadedPathLoadAgainPreviousPath() {
+        subject.setLoading(PATH_A);
+        subject.setLoading(PATH_B);
+        subject.setLoaded();
+        assertLoaded(PATH_A, false);
+    }
+
+    @Test
+    public void testSetLoadedInit() {
+        subject.setInit();
+        subject.setLoaded();
+        assertLoaded(true);
+    }
+
+    @Test
+    public void testIsStoppedConstructor() {
+        assertStopped(true);
+    }
+
+    @Test
+    public void testIsStoppedPaused() {
+        subject.setPaused();
+        assertStopped(false);
+    }
+
+    @Test
+    public void testIsStoppedDeinit() {
+        subject.setPlaying();
+        subject.setDeinit();
+        assertStopped(true);
+    }
+
+    @Test
+    public void testSetStopped() {
+        subject.setPlaying();
+        subject.setStopped();
+        assertStopped(true);
+    }
+
+    @Test
+    public void testIsPlayingConstructor() {
+        assertPlaying(false);
+    }
+
+    @Test
+    public void testIsPlayingPaused() {
+        subject.setPaused();
+        assertPlaying(false);
+    }
+
+    @Test
+    public void testIsPlayingDeinit() {
+        subject.setPlaying();
+        subject.setDeinit();
+        assertPlaying(false);
+    }
+
+    @Test
+    public void testSetPlaying() {
+        subject.setPlaying();
+        assertPlaying(true);
+    }
+
+    @Test
+    public void testIsPausedConstructor() {
+        assertPaused(false);
+    }
+
+    @Test
+    public void testIsPausedStopped() {
+        subject.setStopped();
+        assertPaused(false);
+    }
+
+    @Test
+    public void testIsPausedDeinit() {
+        subject.setPaused();
+        subject.setDeinit();
+        assertPaused(false);
+    }
+
+    @Test
+    public void testSetPaused() {
+        subject.setPaused();
+        assertPaused(true);
+    }
+
+    private void assertInit(boolean init) {
+        assertThat(subject.isInit(), is(init));
+    }
+
+    private void assertLoaded(boolean loaded) {
+        assertThat(subject.isLoaded(), is(loaded));
+    }
+
+    private void assertLoaded(String path, boolean loaded) {
+        assertThat(subject.isLoaded(path), is(loaded));
+    }
+
+    private void assertStopped(boolean stopped) {
+        assertThat(subject.isStopped(), is(stopped));
+    }
+
+    private void assertPlaying(boolean playing) {
+        assertThat(subject.isPlaying(), is(playing));
+    }
+
+    private void assertPaused(boolean paused) {
+        assertThat(subject.isPaused(), is(paused));
+    }
+
+    private void assertPath(String path) {
+        assertThat(subject.getPath(), is(path));
+    }
+
+    private void assertDeinit(boolean deinit) {
+        assertThat(subject.isDeinit(), is(deinit));
+    }
+}
\ No newline at end of file

-- 
To view, visit https://gerrit.wikimedia.org/r/250903
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I4cf7b0be940085aad1a6931d5eee3215de80488c
Gerrit-PatchSet: 1
Gerrit-Project: apps/android/wikipedia
Gerrit-Branch: master
Gerrit-Owner: Niedzielski <sniedziel...@wikimedia.org>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to