Initial commit for MediaControlViewImpl

Test: build
Change-Id: I83c160d2b0dca24b0fe3a2d9af5043e958ac8295
diff --git a/packages/MediaComponents/res/drawable/ic_replay.xml b/packages/MediaComponents/res/drawable/ic_replay.xml
new file mode 100644
index 0000000..2bde120
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/ic_replay.xml
@@ -0,0 +1,4 @@
+<vector android:height="40dp" android:viewportHeight="48.0"
+    android:viewportWidth="48.0" android:width="40dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#FFFFFF" android:pathData="M24,10V2L14,12l10,10v-8c6.63,0 12,5.37 12,12s-5.37,12 -12,12 -12,-5.37 -12,-12H8c0,8.84 7.16,16 16,16s16,-7.16 16,-16 -7.16,-16 -16,-16z"/>
+</vector>
diff --git a/packages/MediaComponents/res/values/strings.xml b/packages/MediaComponents/res/values/strings.xml
index e93269c..35db0e5 100644
--- a/packages/MediaComponents/res/values/strings.xml
+++ b/packages/MediaComponents/res/values/strings.xml
@@ -84,6 +84,7 @@
 
     <string name="lockscreen_pause_button_content_description">Pause</string>
     <string name="lockscreen_play_button_content_description">Play</string>
+    <string name="lockscreen_replay_button_content_description">Replay</string>
 
     <!-- Text for error alert when a video container is not valid for progressive download/playback. -->
     <string name="VideoView2_error_text_invalid_progressive_playback">This video isn\'t valid for streaming to this device.</string>
diff --git a/packages/MediaComponents/src/com/android/widget/MediaControlView2Impl.java b/packages/MediaComponents/src/com/android/widget/MediaControlView2Impl.java
index 8053245e..317a45f 100644
--- a/packages/MediaComponents/src/com/android/widget/MediaControlView2Impl.java
+++ b/packages/MediaComponents/src/com/android/widget/MediaControlView2Impl.java
@@ -16,144 +16,664 @@
 
 package com.android.widget;
 
+import android.content.res.Resources;
+import android.media.MediaMetadata;
 import android.media.session.MediaController;
+import android.media.session.PlaybackState;
 import android.media.update.MediaControlView2Provider;
 import android.media.update.ViewProvider;
+import android.util.Log;
 import android.view.KeyEvent;
+import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
+import android.view.accessibility.AccessibilityManager;
+import android.widget.ImageButton;
 import android.widget.MediaControlView2;
+import android.widget.ProgressBar;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.TextView;
+
+import com.android.media.update.ApiHelper;
+import com.android.media.update.R;
+
+import java.util.Formatter;
+import java.util.Locale;
 
 public class MediaControlView2Impl implements MediaControlView2Provider {
+    private static final String TAG = "MediaControlView2";
+
     private final MediaControlView2 mInstance;
     private final ViewProvider mSuperProvider;
 
     static final String ACTION_SHOW_SUBTITLE = "showSubtitle";
     static final String ACTION_HIDE_SUBTITLE = "hideSubtitle";
 
+    private static final int MAX_PROGRESS = 1000;
+    private static final int DEFAULT_PROGRESS_UPDATE_TIME_MS = 1000;
+    private static final int DEFAULT_TIMEOUT_MS = 2000;
+
+    private static final int REWIND_TIME_MS = 10000;
+    private static final int FORWARD_TIME_MS = 30000;
+
+    private final boolean mUseClosedCaption;
+    private final AccessibilityManager mAccessibilityManager;
+
+    private MediaController mController;
+    private MediaController.TransportControls mControls;
+    private PlaybackState mPlaybackState;
+    private MediaMetadata mMetadata;
+    private ProgressBar mProgress;
+    private TextView mEndTime, mCurrentTime;
+    private int mDuration;
+    private int mPrevState;
+    private boolean mShowing;
+    private boolean mDragging;
+    private boolean mListenersSet;
+    private boolean mCCIsEnabled;
+    private boolean mIsStopped;
+    private View.OnClickListener mNextListener, mPrevListener;
+    private ImageButton mPlayPauseButton;
+    private ImageButton mFfwdButton;
+    private ImageButton mRewButton;
+    private ImageButton mNextButton;
+    private ImageButton mPrevButton;
+    private ImageButton mCCButton;
+    private CharSequence mPlayDescription;
+    private CharSequence mPauseDescription;
+    private CharSequence mReplayDescription;
+
+    private StringBuilder mFormatBuilder;
+    private Formatter mFormatter;
+
     public MediaControlView2Impl(MediaControlView2 instance, ViewProvider superProvider) {
         mInstance = instance;
         mSuperProvider = superProvider;
+        mUseClosedCaption = true;
+        mAccessibilityManager = AccessibilityManager.getInstance(mInstance.getContext());
 
-        // TODO: Implement
+        // Inflate MediaControlView2 from XML
+        View root = makeControllerView();
+        mInstance.addView(root);
     }
 
     @Override
     public void setController_impl(MediaController controller) {
-        // TODO: Implement
+        mController = controller;
+        if (controller != null) {
+            mControls = controller.getTransportControls();
+            // Set mMetadata and mPlaybackState to existing MediaSession variables since they may
+            // be called before the callback is called
+            mPlaybackState = mController.getPlaybackState();
+            mMetadata = mController.getMetadata();
+            updateDuration();
+
+            mController.registerCallback(new MediaControllerCallback());
+        }
     }
 
     @Override
     public void show_impl() {
-        // TODO: Implement
+        mInstance.show(DEFAULT_TIMEOUT_MS);
     }
 
     @Override
     public void show_impl(int timeout) {
-        // TODO: Implement
+        if (!mShowing) {
+            setProgress();
+            if (mPlayPauseButton != null) {
+                mPlayPauseButton.requestFocus();
+            }
+            disableUnsupportedButtons();
+            mInstance.setVisibility(View.VISIBLE);
+            mShowing = true;
+        }
+        // cause the progress bar to be updated even if mShowing
+        // was already true.  This happens, for example, if we're
+        // paused with the progress bar showing the user hits play.
+        mInstance.post(mShowProgress);
+
+        if (timeout != 0 && !mAccessibilityManager.isTouchExplorationEnabled()) {
+            mInstance.removeCallbacks(mFadeOut);
+            mInstance.postDelayed(mFadeOut, timeout);
+        }
     }
 
     @Override
     public boolean isShowing_impl() {
-        // TODO: Implement
-        return false;
+        return mShowing;
     }
 
     @Override
     public void hide_impl() {
-        // TODO: Implement
+        if (mShowing) {
+            try {
+                mInstance.removeCallbacks(mShowProgress);
+                // Remove existing call to mFadeOut to avoid from being called later.
+                mInstance.removeCallbacks(mFadeOut);
+                mInstance.setVisibility(View.GONE);
+            } catch (IllegalArgumentException ex) {
+                Log.w(TAG, "already removed");
+            }
+            mShowing = false;
+        }
     }
 
     @Override
     public void showCCButton_impl() {
-        // TODO: Implement
+        if (mCCButton != null) {
+            mCCButton.setVisibility(View.VISIBLE);
+        }
     }
 
     @Override
     public boolean isPlaying_impl() {
-        // TODO: Implement
+        if (mPlaybackState != null) {
+            return mPlaybackState.getState() == PlaybackState.STATE_PLAYING;
+        }
         return false;
     }
 
     @Override
     public int getCurrentPosition_impl() {
-        // TODO: Implement
+        mPlaybackState = mController.getPlaybackState();
+        if (mPlaybackState != null) {
+            return (int) mPlaybackState.getPosition();
+        }
         return 0;
     }
 
     @Override
     public int getBufferPercentage_impl() {
-        // TODO: Implement
+        if (mDuration == 0) {
+            return 0;
+        }
+        mPlaybackState = mController.getPlaybackState();
+        if (mPlaybackState != null) {
+            return (int) (mPlaybackState.getBufferedPosition() * 100) / mDuration;
+        }
         return 0;
     }
 
     @Override
     public boolean canPause_impl() {
-        // TODO: Implement
-        return false;
+        if (mPlaybackState != null) {
+            return (mPlaybackState.getActions() & PlaybackState.ACTION_PAUSE) != 0;
+        }
+        return true;
     }
 
     @Override
     public boolean canSeekBackward_impl() {
-        // TODO: Implement
-        return false;
+        if (mPlaybackState != null) {
+            return (mPlaybackState.getActions() & PlaybackState.ACTION_REWIND) != 0;
+        }
+        return true;
     }
 
     @Override
     public boolean canSeekForward_impl() {
-        // TODO: Implement
-        return false;
+        if (mPlaybackState != null) {
+            return (mPlaybackState.getActions() & PlaybackState.ACTION_FAST_FORWARD) != 0;
+        }
+        return true;
     }
 
     @Override
     public void showSubtitle_impl() {
-        // TODO: Implement
+        mController.sendCommand(ACTION_SHOW_SUBTITLE, null, null);
     }
 
     @Override
     public void hideSubtitle_impl() {
-        // TODO: Implement
+        mController.sendCommand(ACTION_HIDE_SUBTITLE, null, null);
     }
 
     @Override
     public CharSequence getAccessibilityClassName_impl() {
-        // TODO: Implement
         return MediaControlView2.class.getName();
     }
 
     @Override
     public boolean onTouchEvent_impl(MotionEvent ev) {
-        // TODO: Implement
-        return mSuperProvider.onTouchEvent_impl(ev);
+        return false;
     }
 
+    // TODO: Should this function be removed?
     @Override
     public boolean onTrackballEvent_impl(MotionEvent ev) {
-        // TODO: Implement
-        return mSuperProvider.onTrackballEvent_impl(ev);
+        mInstance.show(DEFAULT_TIMEOUT_MS);
+        return false;
     }
 
     @Override
     public boolean onKeyDown_impl(int keyCode, KeyEvent event) {
-        // TODO: Implement
         return mSuperProvider.onKeyDown_impl(keyCode, event);
     }
 
     @Override
     public void onFinishInflate_impl() {
         mSuperProvider.onFinishInflate_impl();
-        // TODO: Implement
     }
 
     @Override
     public boolean dispatchKeyEvent_impl(KeyEvent event) {
-        // TODO: Implement
+        int keyCode = event.getKeyCode();
+        final boolean uniqueDown = event.getRepeatCount() == 0
+                && event.getAction() == KeyEvent.ACTION_DOWN;
+        if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK
+                || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
+                || keyCode == KeyEvent.KEYCODE_SPACE) {
+            if (uniqueDown) {
+                togglePausePlayState();
+                mInstance.show(DEFAULT_TIMEOUT_MS);
+                if (mPlayPauseButton != null) {
+                    mPlayPauseButton.requestFocus();
+                }
+            }
+            return true;
+        } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
+            if (uniqueDown && !mInstance.isPlaying()) {
+                togglePausePlayState();
+                mInstance.show(DEFAULT_TIMEOUT_MS);
+            }
+            return true;
+        } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
+                || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
+            if (uniqueDown && mInstance.isPlaying()) {
+                togglePausePlayState();
+                mInstance.show(DEFAULT_TIMEOUT_MS);
+            }
+            return true;
+        } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
+                || keyCode == KeyEvent.KEYCODE_VOLUME_UP
+                || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE
+                || keyCode == KeyEvent.KEYCODE_CAMERA) {
+            // don't show the controls for volume adjustment
+            return mSuperProvider.dispatchKeyEvent_impl(event);
+        } else if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU) {
+            if (uniqueDown) {
+                mInstance.hide();
+            }
+            return true;
+        }
+
+        mInstance.show(DEFAULT_TIMEOUT_MS);
         return mSuperProvider.dispatchKeyEvent_impl(event);
     }
 
     @Override
     public void setEnabled_impl(boolean enabled) {
+        if (mPlayPauseButton != null) {
+            mPlayPauseButton.setEnabled(enabled);
+        }
+        if (mFfwdButton != null) {
+            mFfwdButton.setEnabled(enabled);
+        }
+        if (mRewButton != null) {
+            mRewButton.setEnabled(enabled);
+        }
+        if (mNextButton != null) {
+            mNextButton.setEnabled(enabled && mNextListener != null);
+        }
+        if (mPrevButton != null) {
+            mPrevButton.setEnabled(enabled && mPrevListener != null);
+        }
+        if (mProgress != null) {
+            mProgress.setEnabled(enabled);
+        }
+        disableUnsupportedButtons();
         mSuperProvider.setEnabled_impl(enabled);
-        // TODO: Implement
     }
-}
+
+    ///////////////////////////////////////////////////
+    // Protected or private methods
+    ///////////////////////////////////////////////////
+
+    /**
+     * Create the view that holds the widgets that control playback.
+     * Derived classes can override this to create their own.
+     *
+     * @return The controller view.
+     * @hide This doesn't work as advertised
+     */
+    protected View makeControllerView() {
+        View root = LayoutInflater.from(mInstance.getContext()).inflate(
+                R.layout.media_controller, null);
+
+        initControllerView(root);
+
+        return root;
+    }
+
+    private void initControllerView(View v) {
+        Resources res = ApiHelper.getLibResources();
+        mPlayDescription = res.getText(R.string.lockscreen_play_button_content_description);
+        mPauseDescription = res.getText(R.string.lockscreen_pause_button_content_description);
+        mReplayDescription = res.getText(R.string.lockscreen_replay_button_content_description);
+        mPlayPauseButton = v.findViewById(R.id.pause);
+        if (mPlayPauseButton != null) {
+            mPlayPauseButton.requestFocus();
+            mPlayPauseButton.setOnClickListener(mPlayPauseListener);
+        }
+
+        // TODO: make the following buttons visible based upon whether they are supported for
+        // individual media files
+        mFfwdButton = v.findViewById(R.id.ffwd);
+        if (mFfwdButton != null) {
+            mFfwdButton.setOnClickListener(mFfwdListener);
+            mFfwdButton.setVisibility(View.GONE);
+        }
+        mRewButton = v.findViewById(R.id.rew);
+        if (mRewButton != null) {
+            mRewButton.setOnClickListener(mRewListener);
+            mRewButton.setVisibility(View.GONE);
+        }
+        mNextButton = v.findViewById(R.id.next);
+        if (mNextButton != null && !mListenersSet) {
+            mNextButton.setVisibility(View.GONE);
+        }
+        mPrevButton = v.findViewById(R.id.prev);
+        if (mPrevButton != null && !mListenersSet) {
+            mPrevButton.setVisibility(View.GONE);
+        }
+
+        // TODO: make CC button visible if the media file has a subtitle track
+        mCCButton = v.findViewById(R.id.cc);
+        if (mCCButton != null) {
+            mCCButton.setOnClickListener(mCCListener);
+            mCCButton.setVisibility(mUseClosedCaption ? View.VISIBLE : View.GONE);
+        }
+
+        mProgress =
+                v.findViewById(R.id.mediacontroller_progress);
+        if (mProgress != null) {
+            if (mProgress instanceof SeekBar) {
+                SeekBar seeker = (SeekBar) mProgress;
+                seeker.setOnSeekBarChangeListener(mSeekListener);
+            }
+            mProgress.setMax(MAX_PROGRESS);
+        }
+
+        mEndTime = v.findViewById(R.id.time);
+        mCurrentTime = v.findViewById(R.id.time_current);
+        mFormatBuilder = new StringBuilder();
+        mFormatter = new Formatter(mFormatBuilder, Locale.getDefault());
+
+        installPrevNextListeners();
+    }
+
+    /**
+     * Disable pause or seek buttons if the stream cannot be paused or seeked.
+     * This requires the control interface to be a MediaPlayerControlExt
+     */
+    private void disableUnsupportedButtons() {
+        try {
+            if (mPlayPauseButton != null && !mInstance.canPause()) {
+                mPlayPauseButton.setEnabled(false);
+            }
+            if (mRewButton != null && !mInstance.canSeekBackward()) {
+                mRewButton.setEnabled(false);
+            }
+            if (mFfwdButton != null && !mInstance.canSeekForward()) {
+                mFfwdButton.setEnabled(false);
+            }
+            // TODO What we really should do is add a canSeek to the MediaPlayerControl interface;
+            // this scheme can break the case when applications want to allow seek through the
+            // progress bar but disable forward/backward buttons.
+            //
+            // However, currently the flags SEEK_BACKWARD_AVAILABLE, SEEK_FORWARD_AVAILABLE,
+            // and SEEK_AVAILABLE are all (un)set together; as such the aforementioned issue
+            // shouldn't arise in existing applications.
+            if (mProgress != null && !mInstance.canSeekBackward() && !mInstance.canSeekForward()) {
+                mProgress.setEnabled(false);
+            }
+        } catch (IncompatibleClassChangeError ex) {
+            // We were given an old version of the interface, that doesn't have
+            // the canPause/canSeekXYZ methods. This is OK, it just means we
+            // assume the media can be paused and seeked, and so we don't disable
+            // the buttons.
+        }
+    }
+
+    private final Runnable mFadeOut = new Runnable() {
+        @Override
+        public void run() {
+            mInstance.hide();
+        }
+    };
+
+    private final Runnable mShowProgress = new Runnable() {
+        @Override
+        public void run() {
+            int pos = setProgress();
+            if (!mDragging && mShowing && mInstance.isPlaying()) {
+                mInstance.postDelayed(mShowProgress,
+                        DEFAULT_PROGRESS_UPDATE_TIME_MS - (pos % DEFAULT_PROGRESS_UPDATE_TIME_MS));
+            }
+        }
+    };
+
+    private String stringForTime(int timeMs) {
+        int totalSeconds = timeMs / 1000;
+
+        int seconds = totalSeconds % 60;
+        int minutes = (totalSeconds / 60) % 60;
+        int hours = totalSeconds / 3600;
+
+        mFormatBuilder.setLength(0);
+        if (hours > 0) {
+            return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString();
+        } else {
+            return mFormatter.format("%02d:%02d", minutes, seconds).toString();
+        }
+    }
+
+    private int setProgress() {
+        if (mController == null || mDragging) {
+            return 0;
+        }
+        int positionOnProgressBar = 0;
+        int currentPosition = mInstance.getCurrentPosition();
+        if (mDuration > 0) {
+            positionOnProgressBar = (int) (MAX_PROGRESS * (long) currentPosition / mDuration);
+        }
+        if (mProgress != null && currentPosition != mDuration) {
+            mProgress.setProgress(positionOnProgressBar);
+            mProgress.setSecondaryProgress(mInstance.getBufferPercentage() * 10);
+        }
+
+        if (mEndTime != null) {
+            mEndTime.setText(stringForTime(mDuration));
+
+        }
+        if (mCurrentTime != null) {
+            mCurrentTime.setText(stringForTime(currentPosition));
+        }
+
+        return currentPosition;
+    }
+
+    private void togglePausePlayState() {
+        if (mInstance.isPlaying()) {
+            mControls.pause();
+            mPlayPauseButton.setImageResource(R.drawable.ic_play_circle_filled);
+            mPlayPauseButton.setContentDescription(mPlayDescription);
+        } else {
+            mControls.play();
+            mPlayPauseButton.setImageResource(R.drawable.ic_pause_circle_filled);
+            mPlayPauseButton.setContentDescription(mPauseDescription);
+        }
+    }
+
+    // There are two scenarios that can trigger the seekbar listener to trigger:
+    //
+    // The first is the user using the touchpad to adjust the posititon of the
+    // seekbar's thumb. In this case onStartTrackingTouch is called followed by
+    // a number of onProgressChanged notifications, concluded by onStopTrackingTouch.
+    // We're setting the field "mDragging" to true for the duration of the dragging
+    // session to avoid jumps in the position in case of ongoing playback.
+    //
+    // The second scenario involves the user operating the scroll ball, in this
+    // case there WON'T BE onStartTrackingTouch/onStopTrackingTouch notifications,
+    // we will simply apply the updated position without suspending regular updates.
+    private final OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() {
+        @Override
+        public void onStartTrackingTouch(SeekBar bar) {
+            mInstance.show(3600000);
+
+            mDragging = true;
+
+            // By removing these pending progress messages we make sure
+            // that a) we won't update the progress while the user adjusts
+            // the seekbar and b) once the user is done dragging the thumb
+            // we will post one of these messages to the queue again and
+            // this ensures that there will be exactly one message queued up.
+            mInstance.removeCallbacks(mShowProgress);
+
+            // Check if playback is currently stopped. In this case, update the pause button to show
+            // the play image instead of the replay image.
+            if (mIsStopped) {
+                mPlayPauseButton.setImageResource(R.drawable.ic_play_circle_filled);
+                mPlayPauseButton.setContentDescription(mPlayDescription);
+                mIsStopped = false;
+            }
+        }
+
+        @Override
+        public void onProgressChanged(SeekBar bar, int progress, boolean fromUser) {
+            if (!fromUser) {
+                // We're not interested in programmatically generated changes to
+                // the progress bar's position.
+                return;
+            }
+            if (mDuration > 0) {
+                int newPosition = (int) (((long) mDuration * progress) / MAX_PROGRESS);
+                mControls.seekTo(newPosition);
+
+                if (mCurrentTime != null) {
+                    mCurrentTime.setText(stringForTime(newPosition));
+                }
+            }
+        }
+
+        @Override
+        public void onStopTrackingTouch(SeekBar bar) {
+            mDragging = false;
+
+            setProgress();
+            mInstance.show(DEFAULT_TIMEOUT_MS);
+
+            // Ensure that progress is properly updated in the future,
+            // the call to show() does not guarantee this because it is a
+            // no-op if we are already showing.
+            mInstance.post(mShowProgress);
+        }
+    };
+
+    private final View.OnClickListener mPlayPauseListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            togglePausePlayState();
+            mInstance.show(DEFAULT_TIMEOUT_MS);
+        }
+    };
+
+    private final View.OnClickListener mRewListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            int pos = mInstance.getCurrentPosition() - REWIND_TIME_MS;
+            mControls.seekTo(pos);
+            setProgress();
+
+            mInstance.show(DEFAULT_TIMEOUT_MS);
+        }
+    };
+
+    private final View.OnClickListener mFfwdListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            int pos = mInstance.getCurrentPosition() + FORWARD_TIME_MS;
+            mControls.seekTo(pos);
+            setProgress();
+
+            mInstance.show(DEFAULT_TIMEOUT_MS);
+        }
+    };
+
+    private final View.OnClickListener mCCListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            if (!mCCIsEnabled) {
+                mCCButton.setImageResource(R.drawable.ic_media_cc_enabled);
+                mCCIsEnabled = true;
+                mInstance.showSubtitle();
+            } else {
+                mCCButton.setImageResource(R.drawable.ic_media_cc_disabled);
+                mCCIsEnabled = false;
+                mInstance.hideSubtitle();
+            }
+        }
+    };
+
+    private void installPrevNextListeners() {
+        if (mNextButton != null) {
+            mNextButton.setOnClickListener(mNextListener);
+            mNextButton.setEnabled(mNextListener != null);
+        }
+
+        if (mPrevButton != null) {
+            mPrevButton.setOnClickListener(mPrevListener);
+            mPrevButton.setEnabled(mPrevListener != null);
+        }
+    }
+
+    private void updateDuration() {
+        if (mMetadata != null) {
+            if (mMetadata.containsKey(MediaMetadata.METADATA_KEY_DURATION)) {
+                mDuration = (int) mMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION);
+                // update progress bar
+                setProgress();
+            }
+        }
+    }
+
+    private class MediaControllerCallback extends MediaController.Callback {
+        @Override
+        public void onPlaybackStateChanged(PlaybackState state) {
+            mPlaybackState = state;
+
+            // Update pause button depending on playback state for the following two reasons:
+            //   1) Need to handle case where app customizes playback state behavior when app
+            //      activity is resumed.
+            //   2) Need to handle case where the media file reaches end of duration.
+            if (mPlaybackState.getState() != mPrevState) {
+                switch (mPlaybackState.getState()) {
+                    case PlaybackState.STATE_PLAYING:
+                        mPlayPauseButton.setImageResource(R.drawable.ic_pause_circle_filled);
+                        mPlayPauseButton.setContentDescription(mPauseDescription);
+                        break;
+                    case PlaybackState.STATE_PAUSED:
+                        mPlayPauseButton.setImageResource(R.drawable.ic_play_circle_filled);
+                        mPlayPauseButton.setContentDescription(mPlayDescription);
+                        break;
+                    case PlaybackState.STATE_STOPPED:
+                        mPlayPauseButton.setImageResource(R.drawable.ic_replay);
+                        mPlayPauseButton.setContentDescription(mReplayDescription);
+                        mIsStopped = true;
+                        break;
+                    default:
+                        break;
+                }
+                mPrevState = mPlaybackState.getState();
+            }
+        }
+
+        @Override
+        public void onMetadataChanged(MediaMetadata metadata) {
+            mMetadata = metadata;
+            updateDuration();
+        }
+    }
+}
\ No newline at end of file