/***
* The MIT License (MIT)
*
* Copyright (c) 2014 Jaydeep
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package nl.changer.audiowife;
import android.content.Context;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.net.Uri;
import android.os.Handler;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.SeekBar;
import android.widget.TextView;
import java.io.IOException;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
/***
* A simple audio player wrapper for Android
***/
public class AudioWife {
private static final String TAG = AudioWife.class.getSimpleName();
/****
* Playback progress update time in milliseconds
****/
private static final int AUDIO_PROGRESS_UPDATE_TIME = 100;
// TODO: externalize the error messages.
private static final String ERROR_PLAYVIEW_NULL = "Play view cannot be null";
private static final String ERROR_PLAYTIME_CURRENT_NEGATIVE = "Current playback time cannot be negative";
private static final String ERROR_PLAYTIME_TOTAL_NEGATIVE = "Total playback time cannot be negative";
/***
* Keep a single copy of this in memory unless required to create a new instance explicitly.
****/
private static AudioWife mAudioWife;
/***
* Audio URI
****/
private static Uri mUri;
private Handler mProgressUpdateHandler;
private MediaPlayer mMediaPlayer;
private SeekBar mSeekBar;
@Deprecated
/***
* Set both current playack time and total runtime
* of the audio in the UI.
*/
private TextView mPlaybackTime;
private View mPlayButton;
private View mPauseButton;
/***
* Indicates the current run-time of the audio being played
*/
private TextView mRunTime;
/***
* Indicates the total duration of the audio being played.
*/
private TextView mTotalTime;
/***
* Set if AudioWife is using the default UI provided with the library.
**/
private boolean mHasDefaultUi;
/****
* Array to hold custom completion listeners
****/
private ArrayList mCompletionListeners = new ArrayList();
private ArrayList mPlayListeners = new ArrayList();
private ArrayList mPauseListeners = new ArrayList();
private Runnable mUpdateProgress = new Runnable() {
public void run() {
if (mSeekBar == null) {
return;
}
if (mProgressUpdateHandler != null && mMediaPlayer.isPlaying()) {
mSeekBar.setProgress((int) mMediaPlayer.getCurrentPosition());
int currentTime = mMediaPlayer.getCurrentPosition();
updatePlaytime(currentTime);
updateRuntime(currentTime);
// repeat the process
mProgressUpdateHandler.postDelayed(this, AUDIO_PROGRESS_UPDATE_TIME);
} else {
// DO NOT update UI if the player is paused
}
}
};
private OnCompletionListener mOnCompletion = new OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
// set UI when audio finished playing
int currentPlayTime = 0;
mSeekBar.setProgress((int) currentPlayTime);
updatePlaytime(currentPlayTime);
updateRuntime(currentPlayTime);
setPlayable();
// ensure that our completion listener fires first.
// This will provide the developer to over-ride our
// completion listener functionality
fireCustomCompletionListeners(mp);
}
};
private View playerUi;
public static AudioWife getInstance() {
if (mAudioWife == null) {
mAudioWife = new AudioWife();
}
return mAudioWife;
}
/***
* Starts playing audio file associated. Before playing the audio, visibility of appropriate UI
* controls is made visible. Calling this method has no effect if the audio is already being
* played.
****/
public void play() {
// if play button itself is null, the whole purpose of AudioWife is
// defeated.
if (mPlayButton == null) {
throw new IllegalStateException(ERROR_PLAYVIEW_NULL);
}
if (mUri == null) {
throw new IllegalStateException("Uri cannot be null. Call init() before calling this method");
}
if (mMediaPlayer == null) {
throw new IllegalStateException("Call init() before calling this method");
}
if (mMediaPlayer.isPlaying()) {
return;
}
mProgressUpdateHandler.postDelayed(mUpdateProgress, AUDIO_PROGRESS_UPDATE_TIME);
// enable visibility of all UI controls.
setViewsVisibility();
mMediaPlayer.start();
setPausable();
}
/**
* Ensure the views are visible before playing the audio.
*/
private void setViewsVisibility() {
if (mSeekBar != null) {
mSeekBar.setVisibility(View.VISIBLE);
}
if (mPlaybackTime != null) {
mPlaybackTime.setVisibility(View.VISIBLE);
}
if (mRunTime != null) {
mRunTime.setVisibility(View.VISIBLE);
}
if (mTotalTime != null) {
mTotalTime.setVisibility(View.VISIBLE);
}
if (mPlayButton != null) {
mPlayButton.setVisibility(View.VISIBLE);
}
if (mPauseButton != null) {
mPauseButton.setVisibility(View.VISIBLE);
}
}
/***
* Pause the audio being played. Calling this method has no effect if the audio is already
* paused
*/
public void pause() {
if (mMediaPlayer == null) {
return;
}
if (mMediaPlayer.isPlaying()) {
mMediaPlayer.pause();
setPlayable();
}
}
@Deprecated
private void updatePlaytime(int currentTime) {
if (mPlaybackTime == null) {
return;
}
if (currentTime < 0) {
throw new IllegalArgumentException(ERROR_PLAYTIME_CURRENT_NEGATIVE);
}
StringBuilder playbackStr = new StringBuilder();
// set the current time
// its ok to show 00:00 in the UI
playbackStr.append(String.format("%02d:%02d", TimeUnit.MILLISECONDS.toMinutes((long) currentTime), TimeUnit.MILLISECONDS.toSeconds((long) currentTime) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes((long) currentTime))));
playbackStr.append("/");
// show total duration.
long totalDuration = 0;
if (mMediaPlayer != null) {
try {
totalDuration = mMediaPlayer.getDuration();
} catch (IllegalStateException e) {
//e.printStackTrace();
} catch (Exception e) {
//e.printStackTrace();
}
}
// set total time as the audio is being played
if (totalDuration != 0) {
playbackStr.append(String.format("%02d:%02d", TimeUnit.MILLISECONDS.toMinutes((long) totalDuration), TimeUnit.MILLISECONDS.toSeconds((long) totalDuration) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes((long) totalDuration))));
} else {
Log.w(TAG, "Something strage this audio track duration in zero");
}
mPlaybackTime.setText(playbackStr);
// DebugLog.i(currentTime + " / " + totalDuration);
}
private void updateRuntime(int currentTime) {
if (mRunTime == null) {
// this view can be null if the user
// does not want to use it. Don't throw
// an exception.
return;
}
if (currentTime < 0) {
throw new IllegalArgumentException(ERROR_PLAYTIME_CURRENT_NEGATIVE);
}
StringBuilder playbackStr = new StringBuilder();
// set the current time
// its ok to show 00:00 in the UI
playbackStr.append(String.format("%02d:%02d", TimeUnit.MILLISECONDS.toMinutes((long) currentTime), TimeUnit.MILLISECONDS.toSeconds((long) currentTime) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes((long) currentTime))));
mRunTime.setText(playbackStr);
// DebugLog.i(currentTime + " / " + totalDuration);
}
private void setTotalTime() {
if (mTotalTime == null) {
// this view can be null if the user
// does not want to use it. Don't throw
// an exception.
return;
}
StringBuilder playbackStr = new StringBuilder();
long totalDuration = 0;
// by this point the media player is brought to ready state
// by the call to init().
if (mMediaPlayer != null) {
try {
totalDuration = mMediaPlayer.getDuration();
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
if (totalDuration < 0) {
throw new IllegalArgumentException(ERROR_PLAYTIME_TOTAL_NEGATIVE);
}
// set total time as the audio is being played
if (totalDuration != 0) {
playbackStr.append(String.format("%02d:%02d", TimeUnit.MILLISECONDS.toMinutes((long) totalDuration), TimeUnit.MILLISECONDS.toSeconds((long) totalDuration) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes((long) totalDuration))));
}
mTotalTime.setText(playbackStr);
}
/***
* Changes audiowife state to enable play functionality.
*/
private void setPlayable() {
if (mPlayButton != null) {
mPlayButton.setVisibility(View.VISIBLE);
}
if (mPauseButton != null) {
mPauseButton.setVisibility(View.GONE);
}
}
/****
* Changes audio wife to enable pause functionality.
*/
private void setPausable() {
if (mPlayButton != null) {
mPlayButton.setVisibility(View.GONE);
}
if (mPauseButton != null) {
mPauseButton.setVisibility(View.VISIBLE);
}
}
/***
* Initialize the audio player. This method should be the first one to be called before starting
* to play audio using {@link nl.changer.audiowife.AudioWife}
*
* @param ctx {@link android.app.Activity} Context
* @param uri Uri of the audio to be played.
****/
public AudioWife init(Context ctx, Uri uri) {
if (uri == null) {
throw new IllegalArgumentException("Uri cannot be null");
}
if (mAudioWife == null) {
mAudioWife = new AudioWife();
}
mUri = uri;
mProgressUpdateHandler = new Handler();
initPlayer(ctx);
return this;
}
/***
* Sets the audio play functionality on click event of this view. You can set {@link android.widget.Button} or
* an {@link android.widget.ImageView} as audio play control
*
* @see nl.changer.audiowife.AudioWife#addOnPauseClickListener(android.view.View.OnClickListener)
****/
public AudioWife setPlayView(View play) {
if (play == null) {
throw new NullPointerException("PlayView cannot be null");
}
if (mHasDefaultUi) {
Log.w(TAG, "Already using default UI. Setting play view will have no effect");
return this;
}
mPlayButton = play;
initOnPlayClick();
return this;
}
private void initOnPlayClick() {
if (mPlayButton == null) {
throw new NullPointerException(ERROR_PLAYVIEW_NULL);
}
// add default click listener to the top
// so that it is the one that gets fired first
mPlayListeners.add(0, new View.OnClickListener() {
@Override
public void onClick(View v) {
play();
}
});
// Fire all the attached listeners
// when the play button is clicked
mPlayButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
for (View.OnClickListener listener : mPlayListeners) {
listener.onClick(v);
}
}
});
}
/***
* Sets the audio pause functionality on click event of the view passed in as a parameter. You
* can set {@link android.widget.Button} or an {@link android.widget.ImageView} as audio pause control. Audio pause
* functionality will be unavailable if this method is not called.
*
* @see nl.changer.audiowife.AudioWife#addOnPauseClickListener(android.view.View.OnClickListener)
****/
public AudioWife setPauseView(View pause) {
if (pause == null) {
throw new NullPointerException("PauseView cannot be null");
}
if (mHasDefaultUi) {
Log.w(TAG, "Already using default UI. Setting pause view will have no effect");
return this;
}
mPauseButton = pause;
initOnPauseClick();
return this;
}
private void initOnPauseClick() {
if (mPauseButton == null) {
throw new NullPointerException("Pause view cannot be null");
}
// add default click listener to the top
// so that it is the one that gets fired first
mPauseListeners.add(0, new View.OnClickListener() {
@Override
public void onClick(View v) {
pause();
}
});
// Fire all the attached listeners
// when the pause button is clicked
mPauseButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
for (View.OnClickListener listener : mPauseListeners) {
listener.onClick(v);
}
}
});
}
/***
* @deprecated Use {@link nl.changer.audiowife.AudioWife#setRuntimeView(android.widget.TextView)} and
* {@link nl.changer.audiowife.AudioWife#setTotalTimeView(android.widget.TextView)} instead.
* Sets current and total playback time. Use this if you have a playback time
* counter in the UI.
****/
public AudioWife setPlaytime(TextView playTime) {
if (mHasDefaultUi) {
Log.w(TAG, "Already using default UI. Setting play time will have no effect");
return this;
}
mPlaybackTime = playTime;
// initialize the playtime to 0
updatePlaytime(0);
return this;
}
/***
* Sets current playback time view. Use this if you have a playback time counter in the UI.
*
* @see nl.changer.audiowife.AudioWife#setTotalTimeView(android.widget.TextView)
****/
public AudioWife setRuntimeView(TextView currentTime) {
if (mHasDefaultUi) {
Log.w(TAG, "Already using default UI. Setting play time will have no effect");
return this;
}
mRunTime = currentTime;
// initialize the playtime to 0
updateRuntime(0);
return this;
}
/***
* Sets the total playback time view. Use this if you have a playback time counter in the UI.
*
* @see nl.changer.audiowife.AudioWife#setRuntimeView(android.widget.TextView)
****/
public AudioWife setTotalTimeView(TextView totalTime) {
if (mHasDefaultUi) {
Log.w(TAG, "Already using default UI. Setting play time will have no effect");
return this;
}
mTotalTime = totalTime;
setTotalTime();
return this;
}
public AudioWife setSeekBar(SeekBar seekbar) {
if (mHasDefaultUi) {
Log.w(TAG, "Already using default UI. Setting seek bar will have no effect");
return this;
}
mSeekBar = seekbar;
initMediaSeekBar();
return this;
}
/****
* Add custom playback completion listener. Adding multiple listeners will queue up all the
* listeners and fire them on media playback completes.
*/
public AudioWife addOnCompletionListener(OnCompletionListener listener) {
// add default click listener to the top
// so that it is the one that gets fired first
mCompletionListeners.add(0, listener);
return this;
}
/****
* Add custom play view click listener. Calling this method multiple times will queue up all the
* listeners and fire them all together when the event occurs.
***/
public AudioWife addOnPlayClickListener(View.OnClickListener listener) {
mPlayListeners.add(listener);
return this;
}
/***
* Add custom pause view click listener. Calling this method multiple times will queue up all
* the listeners and fire them all together when the event occurs.
***/
public AudioWife addOnPauseClickListener(View.OnClickListener listener) {
mPauseListeners.add(listener);
return this;
}
/****
* Initialize and prepare the audio player
****/
private void initPlayer(Context ctx) {
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
try {
mMediaPlayer.setDataSource(ctx, mUri);
} catch (Exception ignored) {
}
try {
mMediaPlayer.prepare();
} catch (Exception ignored) {
}
mMediaPlayer.setOnCompletionListener(mOnCompletion);
}
private void initMediaSeekBar() {
if (mSeekBar == null) {
return;
}
// update seekbar
long finalTime = mMediaPlayer.getDuration();
mSeekBar.setMax((int) finalTime);
mSeekBar.setProgress(0);
mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
mMediaPlayer.seekTo(seekBar.getProgress());
// if the audio is paused and seekbar is moved,
// update the play time in the UI.
updateRuntime(seekBar.getProgress());
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
}
});
}
private void fireCustomCompletionListeners(MediaPlayer mp) {
for (OnCompletionListener listener : mCompletionListeners) {
listener.onCompletion(mp);
}
}
public void cleanPlayerUi() {
((ViewGroup) playerUi.getParent()).removeView(playerUi);
}
public View getPlayerUi() {
return playerUi;
}
public AudioWife useDefaultUi(ViewGroup playerContainer, LayoutInflater inflater) {
if (playerContainer == null) {
throw new NullPointerException("Player container cannot be null");
}
if (inflater == null) {
throw new IllegalArgumentException("Inflater cannot be null");
}
playerUi = inflater.inflate(R.layout.aw_player, playerContainer, false); // IMPORTANT, sonst geht meine Lösung nicht xD
// init play view
View playView = playerUi.findViewById(R.id.play);
setPlayView(playView);
// init pause view
View pauseView = playerUi.findViewById(R.id.pause);
setPauseView(pauseView);
// init seekbar
SeekBar seekBar = (SeekBar) playerUi.findViewById(R.id.media_seekbar);
setSeekBar(seekBar);
// init playback time view
TextView playbackTime = (TextView) playerUi.findViewById(R.id.playback_time);
setPlaytime(playbackTime);
// this has to be set after all the views
// have finished initializing.
mHasDefaultUi = true;
return this;
}
public void release() {
if (mMediaPlayer != null) {
mMediaPlayer.stop();
mMediaPlayer.reset();
mMediaPlayer.release();
mMediaPlayer = null;
mProgressUpdateHandler = null;
}
}
}