package com.mercandalli.android.apps.files.file.audio;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import android.widget.RemoteViews;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.wearable.Node;
import com.google.android.gms.wearable.NodeApi;
import com.google.android.gms.wearable.Wearable;
import com.mercandalli.android.apps.files.R;
import com.mercandalli.android.apps.files.shared.SharedAudioData;
import com.mercandalli.android.apps.files.shared.SharedAudioPlayerUtils;
import com.mercandalli.android.library.base.graphics.BitmapUtils;
import com.mercandalli.android.library.base.java.StringUtils;
import com.mercandalli.android.library.base.precondition.Preconditions;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import java.util.concurrent.TimeUnit;
import static com.mercandalli.android.apps.files.file.audio.NotificationAudioPlayerReceiver.getNotificationIntentActivity;
import static com.mercandalli.android.apps.files.file.audio.NotificationAudioPlayerReceiver.getNotificationIntentClose;
import static com.mercandalli.android.apps.files.file.audio.NotificationAudioPlayerReceiver.getNotificationIntentNext;
import static com.mercandalli.android.apps.files.file.audio.NotificationAudioPlayerReceiver.getNotificationIntentPause;
import static com.mercandalli.android.apps.files.file.audio.NotificationAudioPlayerReceiver.getNotificationIntentPlayPause;
import static com.mercandalli.android.apps.files.file.audio.NotificationAudioPlayerReceiver.getNotificationIntentPrevious;
/**
* The {@link FileAudioModel} player.
*/
public class FileAudioPlayerManager implements
MediaPlayer.OnPreparedListener,
MediaPlayer.OnCompletionListener,
AudioManager.OnAudioFocusChangeListener {
private static final String TAG = "FileAudioPlayerManager";
private static final int NOTIFICATION_ID = 0;
@Nullable
private static FileAudioPlayerManager sInstance;
@NonNull
public static FileAudioPlayerManager getInstance(@NonNull final Context context) {
if (sInstance == null) {
sInstance = new FileAudioPlayerManager(context);
}
return sInstance;
}
@SharedAudioPlayerUtils.Status
private int mCurrentStatus = SharedAudioPlayerUtils.AUDIO_PLAYER_STATUS_UNKNOWN;
@Nullable
private FileAudioModel mCurrentMusic;
@Nullable
private FileAudioModel mPreparingMusic;
private int mCurrentMusicIndex;
@Nullable
private String mWatchNodeId;
@NonNull
private final List<FileAudioModel> mFileAudioModelList = new ArrayList<>();
@NonNull
private final MediaPlayer mMediaPlayer;
@NonNull
private final Context mContext;
@NonNull
private final AudioManager mAudioManager;
@NonNull
private final UpdaterPosition mUpdatePositionRunnable = new UpdaterPosition();
@NonNull
private final List<OnPlayerStatusChangeListener> mOnPlayerStatusChangeListeners = new ArrayList<>();
@NonNull
private final Handler mHandler = new Handler();
private FileAudioPlayerManager(@NonNull final Context context) {
mContext = context.getApplicationContext();
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setOnPreparedListener(this);
mMediaPlayer.setOnCompletionListener(this);
mCurrentStatus = SharedAudioPlayerUtils.AUDIO_PLAYER_STATUS_PAUSED;
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
notifyPositionChanged();
retrieveDeviceNode(mContext);
// Register the local broadcast receiver, defined in step 3.
final IntentFilter messageFilter = new IntentFilter(Intent.ACTION_SEND);
final MessageReceiver messageReceiver = new MessageReceiver();
LocalBroadcastManager.getInstance(mContext).registerReceiver(messageReceiver, messageFilter);
}
@Override
public void onCompletion(final MediaPlayer mp) {
next();
}
@Override
public void onPrepared(MediaPlayer mp) {
mCurrentMusic = mPreparingMusic;
mPreparingMusic = null;
setCurrentStatus(SharedAudioPlayerUtils.AUDIO_PLAYER_STATUS_PAUSED, false);
play();
}
@Override
public void onAudioFocusChange(final int focusChange) {
if ((mCurrentStatus == SharedAudioPlayerUtils.AUDIO_PLAYER_STATUS_PLAYING) &&
(focusChange == AudioManager.AUDIOFOCUS_LOSS ||
focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT)) {
pause();
}
}
/**
* Play the element #{@link #mCurrentMusicIndex} in {@link #mFileAudioModelList}.
*/
public void play() {
if (isPlayerKO()) {
return;
}
if (SharedAudioPlayerUtils.AUDIO_PLAYER_STATUS_PAUSED == mCurrentStatus) {
final int request = mAudioManager.requestAudioFocus(
this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
if (request == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
mMediaPlayer.start();
setCurrentStatus(SharedAudioPlayerUtils.AUDIO_PLAYER_STATUS_PLAYING);
} else {
setCurrentStatus(SharedAudioPlayerUtils.AUDIO_PLAYER_STATUS_PAUSED);
}
}
}
/**
* Pause the element #{@link #mCurrentMusicIndex} in {@link #mFileAudioModelList}.
*/
public void pause() {
if (isPlayerKO()) {
return;
}
if (SharedAudioPlayerUtils.AUDIO_PLAYER_STATUS_PLAYING == mCurrentStatus) {
mMediaPlayer.pause();
setCurrentStatus(SharedAudioPlayerUtils.AUDIO_PLAYER_STATUS_PAUSED);
}
mAudioManager.abandonAudioFocus(this);
}
/**
* Play the next {@link FileAudioModel} in {@link #mFileAudioModelList}.
*/
public void next() {
if (isPlayerKO()) {
return;
}
mCurrentMusicIndex++;
if (mCurrentMusicIndex >= mFileAudioModelList.size()) {
mCurrentMusicIndex = 0;
}
final FileAudioModel currentMusic = mFileAudioModelList.get(mCurrentMusicIndex);
if (mCurrentMusic == null || !StringUtils.isEquals(currentMusic.getPath(), mCurrentMusic.getPath())) {
prepare(currentMusic);
}
notifyAudioChanged(SharedAudioPlayerUtils.AUDIO_PLAYER_ACTION_NEXT);
}
/**
* Play the previous {@link FileAudioModel} in {@link #mFileAudioModelList}.
*/
public void previous() {
if (isPlayerKO()) {
return;
}
mCurrentMusicIndex--;
if (mCurrentMusicIndex < 0) {
mCurrentMusicIndex = mFileAudioModelList.size() - 1;
}
final FileAudioModel currentMusic = mFileAudioModelList.get(mCurrentMusicIndex);
if (mCurrentMusic == null || !StringUtils.isEquals(currentMusic.getPath(), mCurrentMusic.getPath())) {
prepare(currentMusic);
}
notifyAudioChanged(SharedAudioPlayerUtils.AUDIO_PLAYER_ACTION_PREVIOUS);
}
/**
* Is the song playing.
*
* @return True if the {@code mCurrentStatus == SharedAudioPlayerUtils.AUDIO_PLAYER_STATUS_PLAYING}.
*/
public boolean isPlaying() {
return mCurrentStatus == SharedAudioPlayerUtils.AUDIO_PLAYER_STATUS_PLAYING;
}
public void startMusic(
int currentMusicIndex,
final List<FileAudioModel> musics) {
Preconditions.checkNotNull(musics);
if (musics.isEmpty()) {
Log.e(TAG, "startMusic with empty List");
return;
}
int musicsSize = musics.size();
if (musicsSize <= currentMusicIndex) {
Log.e(TAG, "startMusic invalid index : " + currentMusicIndex + " with musics.size() = " + musicsSize);
currentMusicIndex = 0;
}
mFileAudioModelList.clear();
mFileAudioModelList.addAll(musics);
FileAudioModel newCurrentMusic = mFileAudioModelList.get(currentMusicIndex);
if (newCurrentMusic.getPath() == null) {
// Error with the newCurrentMusic: we play the first track.
mCurrentMusicIndex = 0;
mFileAudioModelList.remove(currentMusicIndex);
newCurrentMusic = mFileAudioModelList.get(mCurrentMusicIndex);
if (newCurrentMusic.getPath() == null) {
// Error.
return;
}
} else {
mCurrentMusicIndex = currentMusicIndex;
}
if (mCurrentMusic == null || !newCurrentMusic.getPath().equals(mCurrentMusic.getPath())) {
if (SharedAudioPlayerUtils.AUDIO_PLAYER_STATUS_PREPARING == mCurrentStatus) {
mMediaPlayer.reset();
setCurrentStatus(SharedAudioPlayerUtils.AUDIO_PLAYER_STATUS_PAUSED, false);
}
prepare(newCurrentMusic);
} else if (mCurrentStatus == SharedAudioPlayerUtils.AUDIO_PLAYER_STATUS_PAUSED) {
play();
}
notifyAudioChanged(SharedAudioPlayerUtils.AUDIO_PLAYER_ACTION_PLAY);
}
public int getDuration() {
if (mPreparingMusic == null && mCurrentMusic != null) {
return mMediaPlayer.getDuration();
}
return 0;
}
public int getCurrentProgress() {
if (mPreparingMusic == null && mCurrentMusic != null) {
return mMediaPlayer.getCurrentPosition();
}
return 0;
}
public void seekTo(int milliseconds) {
if (mPreparingMusic == null && mCurrentMusic != null) {
mMediaPlayer.seekTo(milliseconds);
}
}
public boolean isEmpty() {
return mFileAudioModelList.isEmpty();
}
public void addOnPlayerStatusChangeListener(OnPlayerStatusChangeListener listener) {
synchronized (mOnPlayerStatusChangeListeners) {
if (!mOnPlayerStatusChangeListeners.contains(listener)) {
mOnPlayerStatusChangeListeners.add(listener);
}
}
}
public void removeOnPreviewPlayerStatusChangeListener(OnPlayerStatusChangeListener listener) {
synchronized (mOnPlayerStatusChangeListeners) {
mOnPlayerStatusChangeListeners.remove(listener);
}
}
public int getCurrentMusicIndex() {
return mCurrentMusicIndex;
}
@NonNull
public List<FileAudioModel> getFileAudioModelList() {
return mFileAudioModelList;
}
/* PRIVATE */
private void notifyPositionChanged() {
mHandler.removeCallbacks(mUpdatePositionRunnable);
if (isPlaying()) {
synchronized (mOnPlayerStatusChangeListeners) {
final ListIterator<OnPlayerStatusChangeListener> listIterator =
mOnPlayerStatusChangeListeners.listIterator();
while (listIterator.hasNext()) {
final OnPlayerStatusChangeListener next = listIterator.next();
if (next.onPlayerProgressChanged(getCurrentProgress(), getDuration())) {
listIterator.remove();
}
}
}
}
mHandler.postDelayed(mUpdatePositionRunnable, 1_000);
}
private void notifyAudioChanged(@SharedAudioPlayerUtils.Action final int action) {
synchronized (mOnPlayerStatusChangeListeners) {
final ListIterator<OnPlayerStatusChangeListener> listIterator =
mOnPlayerStatusChangeListeners.listIterator();
while (listIterator.hasNext()) {
final OnPlayerStatusChangeListener next = listIterator.next();
if (next.onAudioChanged(
mCurrentMusicIndex,
mFileAudioModelList,
action)) {
listIterator.remove();
}
}
}
}
private void prepare(@NonNull final FileAudioModel fileAudioModel) {
Preconditions.checkNotNull(fileAudioModel);
if (SharedAudioPlayerUtils.AUDIO_PLAYER_STATUS_PREPARING == mCurrentStatus || isPlayerKO()) {
return;
}
mPreparingMusic = fileAudioModel;
setCurrentStatus(SharedAudioPlayerUtils.AUDIO_PLAYER_STATUS_PREPARING);
if (mMediaPlayer.isPlaying()) {
mMediaPlayer.stop();
}
mMediaPlayer.reset();
try {
mMediaPlayer.setDataSource(fileAudioModel.getPath());
mMediaPlayer.prepareAsync();
} catch (IOException e) {
mMediaPlayer.reset();
setCurrentStatus(SharedAudioPlayerUtils.AUDIO_PLAYER_STATUS_PAUSED);
}
}
private void setCurrentStatus(final int currentStatus) {
setCurrentStatus(currentStatus, true);
}
private void setCurrentStatus(final int currentStatus, final boolean notifyListeners) {
mCurrentStatus = currentStatus;
setNotification(currentStatus == SharedAudioPlayerUtils.AUDIO_PLAYER_STATUS_PLAYING);
sendWearMessage(mContext, currentStatus, mFileAudioModelList.get(mCurrentMusicIndex));
if (notifyListeners) {
synchronized (mOnPlayerStatusChangeListeners) {
final ListIterator<OnPlayerStatusChangeListener> listIterator =
mOnPlayerStatusChangeListeners.listIterator();
while (listIterator.hasNext()) {
final OnPlayerStatusChangeListener next = listIterator.next();
if (next.onPlayerStatusChanged(mCurrentStatus)) {
listIterator.remove();
}
}
}
}
}
/**
* Display or hide the notification.
*/
/* package */
void setNotification(final boolean activated) {
if (activated && mCurrentMusic != null) {
if (mMediaPlayer.isPlaying()) {
final RemoteViews remoteViews = new RemoteViews(mContext.getPackageName(),
R.layout.notification_musique);
remoteViews.setTextViewText(R.id.titre_notif,
mCurrentMusic.getName());
remoteViews.setOnClickPendingIntent(R.id.titre_notif,
getNotificationIntentActivity(mContext));
remoteViews.setOnClickPendingIntent(R.id.close,
getNotificationIntentClose(mContext));
remoteViews.setOnClickPendingIntent(R.id.activity_file_audio_play,
getNotificationIntentPlayPause(mContext));
remoteViews.setOnClickPendingIntent(R.id.activity_file_audio_next,
getNotificationIntentNext(mContext));
remoteViews.setOnClickPendingIntent(R.id.prev,
getNotificationIntentPrevious(mContext));
NotificationManagerCompat.from(mContext).notify(NOTIFICATION_ID,
new NotificationCompat.Builder(mContext)
.setSmallIcon(R.drawable.ic_music_note_white_24dp)
.setAutoCancel(false)
//.setOngoing(true)
.setContent(remoteViews)
.addAction(R.mipmap.ic_launcher,
"Next",
getNotificationIntentNext(mContext))
.addAction(R.mipmap.ic_launcher,
"Play/Pause",
getNotificationIntentPlayPause(mContext))
.addAction(R.mipmap.ic_launcher,
"Previous",
getNotificationIntentPrevious(mContext))
.extend(new NotificationCompat.WearableExtender()
.setBackground(BitmapUtils.drawableToBitmap(
ContextCompat.getDrawable(
mContext,
R.drawable.ic_music_note_white_24dp))))
.setDeleteIntent(getNotificationIntentPause(mContext))
.setContentIntent(PendingIntent.getActivity(mContext, 0,
new Intent(mContext, FileAudioActivity.class),
PendingIntent.FLAG_UPDATE_CURRENT))
.build());
}
} else {
NotificationManagerCompat.from(mContext).cancel(NOTIFICATION_ID);
}
}
@NonNull
private GoogleApiClient getGoogleApiClient(@NonNull final Context context) {
Preconditions.checkNotNull(context);
return new GoogleApiClient.Builder(context)
.addApi(Wearable.API)
.build();
}
private void retrieveDeviceNode(@NonNull final Context context) {
Preconditions.checkNotNull(context);
final GoogleApiClient client = getGoogleApiClient(context);
new Thread(new Runnable() {
@Override
public void run() {
client.blockingConnect(FileAudioWearUtils.CONNECTION_TIME_OUT_MS, TimeUnit.MILLISECONDS);
NodeApi.GetConnectedNodesResult result =
Wearable.NodeApi.getConnectedNodes(client).await();
final List<Node> nodes = result.getNodes();
if (nodes.size() > 0) {
mWatchNodeId = nodes.get(0).getId();
}
client.disconnect();
}
}).start();
}
private void sendWearMessage(
@NonNull final Context context,
@SharedAudioPlayerUtils.Status final int currentStatus,
@NonNull final FileAudioModel fileAudioModel) {
Preconditions.checkNotNull(context);
Preconditions.checkNotNull(fileAudioModel);
FileAudioWearUtils.sendWearMessage(
getGoogleApiClient(context), mWatchNodeId, currentStatus, fileAudioModel);
}
/**
* Is this player KO. Are the {@link #mFileAudioModelList}, {@link #mCurrentMusicIndex} KO.
*
* @return If this class is KO.
*/
private boolean isPlayerKO() {
boolean playerKO = false;
if (mFileAudioModelList.isEmpty()) {
Log.e(TAG, "mFileAudioModelList is empty");
playerKO = true;
} else if (mCurrentMusicIndex >= mFileAudioModelList.size()) {
Log.e(TAG, "mCurrentMusicIndex >= mFileAudioModelList.size()");
playerKO = true;
}
return playerKO;
}
/* INNER */
private class UpdaterPosition implements Runnable {
@Override
public void run() {
notifyPositionChanged();
}
}
public class MessageReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
final String message = intent.getStringExtra("message");
if (message.isEmpty() || message.replaceAll(" ", "").isEmpty()) {
sendWearMessage(mContext, mCurrentStatus, mFileAudioModelList.get(mCurrentMusicIndex));
return;
}
if (mFileAudioModelList.isEmpty()) {
setNotification(false);
return;
}
final SharedAudioData sharedAudioData = new SharedAudioData(message);
switch (sharedAudioData.getAction()) {
case SharedAudioPlayerUtils.AUDIO_PLAYER_ACTION_PAUSE:
pause();
break;
case SharedAudioPlayerUtils.AUDIO_PLAYER_ACTION_PLAY:
play();
break;
case SharedAudioPlayerUtils.AUDIO_PLAYER_ACTION_NEXT:
next();
break;
case SharedAudioPlayerUtils.AUDIO_PLAYER_ACTION_PREVIOUS:
previous();
break;
case SharedAudioPlayerUtils.AUDIO_PLAYER_ACTION_UNKNOWN:
break;
}
}
}
public interface OnPlayerStatusChangeListener {
/**
* The player status change.
*
* @param status The new status.
* @return <code>true</code> if the caller has to remove the listener, otherwise keep this
* listener in the {@link List} of listeners.
*/
boolean onPlayerStatusChanged(@SharedAudioPlayerUtils.Status final int status);
/**
* The player progress change.
*
* @return <code>true</code> if the caller has to remove the listener, otherwise keep this
* listener in the {@link List} of listeners.
*/
boolean onPlayerProgressChanged(final int progress, final int duration);
/**
* @return <code>true</code> if the caller has to remove the listener, otherwise keep this
* listener in the {@link List} of listeners.
*/
boolean onAudioChanged(
final int musicPosition,
final List<FileAudioModel> musics,
@SharedAudioPlayerUtils.Action final int action);
}
}