X-Git-Url: https://git.mdrn.pl/wl-app.git/blobdiff_plain/48b2fe9f7c2dc3d9aeaaa6dbfb27c7da4f3235ff..269195b3729c1bdc22e9053ee4ebca667ea8549d:/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/MediaPlayerAdapter.java diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/MediaPlayerAdapter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/MediaPlayerAdapter.java new file mode 100755 index 0000000..2bb3a57 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/MediaPlayerAdapter.java @@ -0,0 +1,327 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.moiseum.wolnelektury.view.player.service; + +import android.content.Context; +import android.content.res.AssetFileDescriptor; +import android.media.MediaPlayer; +import android.os.Bundle; +import android.os.Handler; +import android.os.SystemClock; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import android.util.Log; + +import com.moiseum.wolnelektury.connection.downloads.FileCacheUtils; + +/** + * Exposes the functionality of the {@link MediaPlayer} and implements the {@link PlayerAdapter} + * so that MainActivity can control music playback. + */ +public final class MediaPlayerAdapter extends PlayerAdapter { + + private class MediaPlayerException extends Exception { + + private final int what; + private final int extra; + + public MediaPlayerException(int what, int extra) { + this.what = what; + this.extra = extra; + } + + @Override + public String getMessage() { + return "Media player failed with code (" + what + "," + extra + ")"; + } + } + + private static final String TAG = MediaPlayerAdapter.class.getSimpleName(); + private static final int FIVE_SECONDS = 5000; + private static final int CALLBACK_INTERVAL = 400; + + private final Context mContext; + private MediaPlayer mMediaPlayer; + private String mFilename; + private PlaybackInfoListener mPlaybackInfoListener; + private MediaMetadataCompat mCurrentMedia; + private int mState; + private boolean mCurrentMediaPlayedToCompletion; + + private Handler handler = new Handler(); + private Runnable positionUpdateRunnable = new Runnable() { + @Override + public void run() { + mPlaybackInfoListener.onPlaybackProgress(mMediaPlayer.getCurrentPosition()); + handler.postDelayed(this, CALLBACK_INTERVAL); + } + }; + + // Work-around for a MediaPlayer bug related to the behavior of MediaPlayer.seekTo() + // while not playing. + private int mSeekWhileNotPlaying = -1; + + MediaPlayerAdapter(Context context, PlaybackInfoListener listener) { + super(context); + mContext = context.getApplicationContext(); + mPlaybackInfoListener = listener; + } + + /** + * Once the {@link MediaPlayer} is released, it can't be used again, and another one has to be + * created. In the onStop() method of the MainActivity the {@link MediaPlayer} is + * released. Then in the onStart() of the MainActivity a new {@link MediaPlayer} + * object has to be created. That's why this method is private, and called by load(int) and + * not the constructor. + */ + private void initializeMediaPlayer() { + if (mMediaPlayer == null) { + mMediaPlayer = new MediaPlayer(); + mMediaPlayer.setOnCompletionListener(mediaPlayer -> { + mPlaybackInfoListener.onPlaybackCompleted(); + + // Set the state to "paused" because it most closely matches the state + // in MediaPlayer with regards to available state transitions compared + // to "stop". + // Paused allows: seekTo(), start(), pause(), stop() + // Stop allows: stop() + setNewState(PlaybackStateCompat.STATE_PAUSED); + }); + mMediaPlayer.setOnErrorListener((mp, what, extra) -> { + Log.e(TAG, "Media player experienced failure: " + what + ", " + extra); + setNewState(PlaybackStateCompat.STATE_ERROR); + return false; + }); + } + } + + // Implements PlaybackControl. + @Override + public void playFromMedia(MediaMetadataCompat metadata) { + mCurrentMedia = metadata; + final String mediaId = metadata.getDescription().getMediaId(); + playFile(AudiobookLibrary.getMusicFilename(mediaId)); + } + + @Override + public MediaMetadataCompat getCurrentMedia() { + return mCurrentMedia; + } + + private void playFile(String filename) { + boolean mediaChanged = (mFilename == null || !filename.equals(mFilename)); + if (mCurrentMediaPlayedToCompletion) { + // Last audio file was played to completion, the resourceId hasn't changed, but the + // player was released, so force a reload of the media file for playback. + mediaChanged = true; + mCurrentMediaPlayedToCompletion = false; + } + if (!mediaChanged) { + if (!isPlaying()) { + play(); + } + return; + } else { + release(); + } + + mFilename = filename; + + initializeMediaPlayer(); + + try { + String cachedFileForUrl = FileCacheUtils.getCachedFileForUrl(mFilename); + mMediaPlayer.setDataSource(cachedFileForUrl); + mMediaPlayer.prepare(); + mPlaybackInfoListener.onPlaybackPrepared(mMediaPlayer.getDuration()); + } catch (Exception e) { + Log.e(TAG, "Failed to load file: " + mFilename, e); + setNewState(PlaybackStateCompat.STATE_ERROR); + } + + play(); + } + + @Override + public void onStop() { + // Regardless of whether or not the MediaPlayer has been created / started, the state must + // be updated, so that MediaNotificationManager can take down the notification. + setNewState(PlaybackStateCompat.STATE_STOPPED); + release(); + } + + private void release() { + if (mMediaPlayer != null) { + handler.removeCallbacks(positionUpdateRunnable); + mMediaPlayer.release(); + mMediaPlayer = null; + } + } + + @Override + public boolean isPlaying() { + return mMediaPlayer != null && mMediaPlayer.isPlaying(); + } + + @Override + protected void onPlay() { + if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) { + mMediaPlayer.start(); + handler.postDelayed(positionUpdateRunnable, CALLBACK_INTERVAL); + setNewState(PlaybackStateCompat.STATE_PLAYING); + } + } + + @Override + protected void onPause() { + if (mMediaPlayer != null && mMediaPlayer.isPlaying()) { + mMediaPlayer.pause(); + handler.removeCallbacks(positionUpdateRunnable); + setNewState(PlaybackStateCompat.STATE_PAUSED); + } + } + + // This is the main reducer for the player state machine. + private void setNewState(@PlaybackStateCompat.State int newPlayerState) { + mState = newPlayerState; + if (mState == PlaybackStateCompat.STATE_ERROR) { + handler.removeCallbacks(positionUpdateRunnable); + mFilename = null; + } + + // Whether playback goes to completion, or whether it is stopped, the + // mCurrentMediaPlayedToCompletion is set to true. + if (mState == PlaybackStateCompat.STATE_STOPPED) { + mCurrentMediaPlayedToCompletion = true; + } + + // Work around for MediaPlayer.getCurrentPosition() when it changes while not playing. + final long reportPosition; + if (mSeekWhileNotPlaying >= 0) { + reportPosition = mSeekWhileNotPlaying; + + if (mState == PlaybackStateCompat.STATE_PLAYING) { + mSeekWhileNotPlaying = -1; + } + } else { + reportPosition = mMediaPlayer == null ? 0 : mMediaPlayer.getCurrentPosition(); + } + + final PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat.Builder(); + stateBuilder.setActions(getAvailableActions()); + stateBuilder.setState(mState, + reportPosition, + 1.0f, + SystemClock.elapsedRealtime()); + if (mState == PlaybackStateCompat.STATE_PLAYING) { + Bundle extras = new Bundle(); + extras.putInt(AudiobookService.EXTRA_PLAYBACK_TOTAL, mMediaPlayer.getDuration()); + stateBuilder.setExtras(extras); + } + mPlaybackInfoListener.onPlaybackStateChange(stateBuilder.build()); + } + + /** + * Set the current capabilities available on this session. Note: If a capability is not + * listed in the bitmask of capabilities then the MediaSession will not handle it. For + * example, if you don't want ACTION_STOP to be handled by the MediaSession, then don't + * included it in the bitmask that's returned. + */ + @PlaybackStateCompat.Actions + private long getAvailableActions() { + long actions = PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID + | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH + | PlaybackStateCompat.ACTION_SKIP_TO_NEXT + | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; + switch (mState) { + case PlaybackStateCompat.STATE_STOPPED: + actions |= PlaybackStateCompat.ACTION_PLAY + | PlaybackStateCompat.ACTION_PAUSE; + break; + case PlaybackStateCompat.STATE_PLAYING: + actions |= PlaybackStateCompat.ACTION_STOP + | PlaybackStateCompat.ACTION_PAUSE + | PlaybackStateCompat.ACTION_SEEK_TO; + break; + case PlaybackStateCompat.STATE_PAUSED: + actions |= PlaybackStateCompat.ACTION_PLAY + | PlaybackStateCompat.ACTION_STOP; + break; + default: + actions |= PlaybackStateCompat.ACTION_PLAY + | PlaybackStateCompat.ACTION_PLAY_PAUSE + | PlaybackStateCompat.ACTION_STOP + | PlaybackStateCompat.ACTION_PAUSE; + } + return actions; + } + + @Override + public void seekTo(long position) { + if (mMediaPlayer != null) { + if (!mMediaPlayer.isPlaying()) { + mSeekWhileNotPlaying = (int) position; + } + mMediaPlayer.seekTo((int) position); + + // Set the state (to the current state) because the position changed and should + // be reported to clients. + setNewState(mState); + } + } + + @Override + public void fastForward() { + if (mMediaPlayer != null) { + int seekTo = mMediaPlayer.getCurrentPosition() + FIVE_SECONDS; + int newState = mState; + + if (seekTo > mMediaPlayer.getDuration()) { + seekTo = mMediaPlayer.getDuration(); + newState = PlaybackStateCompat.STATE_PAUSED; + mMediaPlayer.pause(); + } + + mMediaPlayer.seekTo(seekTo); + setNewState(newState); + } + } + + @Override + public void rewind() { + if (mMediaPlayer != null) { + int seekTo = mMediaPlayer.getCurrentPosition() - FIVE_SECONDS; + int newState = mState; + + if (seekTo < 0) { + seekTo = 0; + newState = PlaybackStateCompat.STATE_PAUSED; + mMediaPlayer.pause(); + } + + mMediaPlayer.seekTo(seekTo); + setNewState(newState); + } + } + + @Override + public void setVolume(float volume) { + if (mMediaPlayer != null) { + mMediaPlayer.setVolume(volume, volume); + } + } +}