package com.wigwamlabs.spotify; import android.content.ComponentName; import android.content.Context; import android.media.AudioManager; import android.os.Handler; import android.speech.tts.TextToSpeech; import android.widget.Toast; import com.wigwamlabs.spotify.tts.TtsProvider; import proguard.annotation.Keep; import java.util.ArrayList; import java.util.Locale; public class Player extends NativeItem implements AudioManager.OnAudioFocusChangeListener { static { nativeInitClass(); } public static final int STATE_STARTED = 0; public static final int STATE_PLAYING = 1; public static final int STATE_PAUSED_USER = 2; public static final int STATE_PAUSED_NOISY = 3; public static final int STATE_PAUSED_AUDIOFOCUS = 4; public static final int STATE_STOPPED = 5; private static final long DURATION_BEFORE_AUDIO_UNRESPONSIVE_MS = 10 * 1000; private final Handler mHandler = new Handler(); private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>(); private final Context mContext; private final ImageProvider mImageProvider; private final AudioManager mAudioManager; private final Runnable mResondToUnresponsiveAudio; private boolean mHasAudioFocus; private Queue mQueue; private int mTrackProgressSec = 0; private int mTrackDurationSec = 0; private RemoteControlClient mRemoteControlClient; private boolean mPrefetchRequested; private TextToSpeech mTts; private boolean mTtsIsInitialized; private TtsProvider mTtsProvider; public Player(Context context, int handle, ImageProvider imageProvider) { super(handle); nativeInitInstance(); mContext = context; mImageProvider = imageProvider; mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); mResondToUnresponsiveAudio = new Runnable() { @Override public void run() { onUnresponsiveAudio(); } }; mTts = new TextToSpeech(context, new TextToSpeech.OnInitListener() { @Override public void onInit(int status) { if (status == TextToSpeech.SUCCESS) { mTts.setLanguage(Locale.US); mTtsIsInitialized = true; } else { mTts.shutdown(); mTts = null; } } }); } private static native void nativeInitClass(); private native void nativeInitInstance(); @Override void nativeDestroy() { // do nothing, native instance is deleted by session } private native int nativeGetState(); private native int nativePlay(Track track); private native void nativePrefetchTrack(Track track); private native void nativePause(int reasonState); private native void nativeResume(); private native void nativeSeek(int progressMs); @Keep private void onStateChanged(final int state) { mHandler.post(new Runnable() { @Override public void run() { for (Callback callback : mCallbacks) { callback.onStateChanged(state); } if (state != STATE_PLAYING && state != STATE_PAUSED_AUDIOFOCUS && state != STATE_PAUSED_USER && state != STATE_PAUSED_NOISY) { abandonAudioFocus(); } if (mRemoteControlClient != null) { mRemoteControlClient.onStateChanged(state); } if (state == STATE_PLAYING) { Debug.logAudioResponsivenessVerbose("Start checking since we start playing"); mHandler.removeCallbacks(mResondToUnresponsiveAudio); mHandler.postDelayed(mResondToUnresponsiveAudio, DURATION_BEFORE_AUDIO_UNRESPONSIVE_MS); } else { Debug.logAudioResponsivenessVerbose("Stop checking, since we're not playing"); mHandler.removeCallbacks(mResondToUnresponsiveAudio); } } }); } @Keep private void onTrackProgress(final int secondsPlayed, final int secondsDuration) { mTrackProgressSec = secondsPlayed; mTrackDurationSec = secondsDuration; mHandler.post(new Runnable() { @Override public void run() { for (Callback callback : mCallbacks) { callback.onTrackProgress(secondsPlayed, secondsDuration); } if (secondsPlayed >= secondsDuration) { mQueue.next(); playTrack(); } else if (secondsDuration - secondsPlayed <= 20 && !mPrefetchRequested) { final Track next = mQueue.getTrack(1); if (next != null) { nativePrefetchTrack(next); } mPrefetchRequested = true; } if (secondsDuration - secondsPlayed <= 2) { if (mTtsIsInitialized && mTtsProvider != null) { final String msg = mTtsProvider.getText(); if (msg != null) { Debug.logTts(msg); mTts.speak(msg, TextToSpeech.QUEUE_FLUSH, null); } } } Debug.logAudioResponsivenessVerbose("Got audio, postpone check"); mHandler.removeCallbacks(mResondToUnresponsiveAudio); mHandler.postDelayed(mResondToUnresponsiveAudio, DURATION_BEFORE_AUDIO_UNRESPONSIVE_MS); } }); } @Keep private void onPlayTokenLost() { mHandler.post(new Runnable() { @Override public void run() { Toast.makeText(mContext, R.string.toast_play_token_lost, Toast.LENGTH_SHORT).show(); } }); } public void addCallback(Callback callback, boolean callbackNow) { if (mCallbacks.contains(callback)) { return; } mCallbacks.add(callback); if (callbackNow) { callback.onStateChanged(getState()); callback.onCurrentTrackUpdated(mQueue != null ? mQueue.getTrack(0) : null); callback.onTrackProgress(mTrackProgressSec, mTrackDurationSec); } } private int getState() { return nativeGetState(); } public void removeCallback(Callback callback) { mCallbacks.remove(callback); } public void play(Queue queue) { if (mQueue != null) { mQueue.destroy(); } mQueue = queue; // TODO if the play call fails we should probably abandon the focus (applies for all uses of focus) if (requestAudioFocus()) { playTrack(); } } private void playTrack() { Track track; while (true) { track = mQueue.getTrack(0); final int error = nativePlay(track); if (error == SpotifyError.TRACK_NOT_PLAYABLE) { Debug.logQueue("Queue: track '" + track.getName() + "' not playable, skip"); //TODO what if all tracks are non playable //TODO keep track of which tracks are playable? mQueue.next(); } else { break; } } mPrefetchRequested = false; for (Callback callback : mCallbacks) { callback.onCurrentTrackUpdated(track); } if (mRemoteControlClient != null) { mRemoteControlClient.updateMediaData(track); } } public void seek(int progressMs) { nativeSeek(progressMs); } public void pause() { nativePause(STATE_PAUSED_USER); } void pauseNoisy() { nativePause(STATE_PAUSED_NOISY); } public void resume() { if (requestAudioFocus()) { nativeResume(); } } public void togglePause() { switch (getState()) { case STATE_PLAYING: pause(); break; case STATE_PAUSED_USER: case STATE_PAUSED_AUDIOFOCUS: case STATE_PAUSED_NOISY: resume(); break; case STATE_STARTED: case STATE_STOPPED: break; } } public void next() { if (mQueue == null) { return; } if (requestAudioFocus()) { mQueue.next(); playTrack(); } } private boolean requestAudioFocus() { if (!mHasAudioFocus) { final int result = mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); mHasAudioFocus = (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED); Debug.logAudioFocus("Audio focus request: " + (mHasAudioFocus ? "succeeded" : "failed")); } if (mHasAudioFocus) { final ComponentName receiver = new ComponentName(mContext, StaticBroadcastReceiver.class); // if the receiver is already registered, it will be moved to the top of the stack, so it's ok to call it multiple times mAudioManager.registerMediaButtonEventReceiver(receiver); if (mRemoteControlClient == null) { mRemoteControlClient = RemoteControlClient.create(mContext, receiver, mImageProvider); } mAudioManager.registerRemoteControlClient(mRemoteControlClient); } return mHasAudioFocus; } private void abandonAudioFocus() { final ComponentName receiver = new ComponentName(mContext, StaticBroadcastReceiver.class); if (mRemoteControlClient != null) { mAudioManager.unregisterRemoteControlClient(mRemoteControlClient); mRemoteControlClient = null; } mAudioManager.unregisterMediaButtonEventReceiver(receiver); if (mHasAudioFocus) { Debug.logAudioFocus("Abandoning audio focus"); mHasAudioFocus = false; mAudioManager.abandonAudioFocus(this); } } @Override public void onAudioFocusChange(int focusChange) { switch (focusChange) { case AudioManager.AUDIOFOCUS_GAIN: Debug.logAudioFocus("Gained audio focus"); mHasAudioFocus = true; if (getState() == STATE_PAUSED_AUDIOFOCUS) { resume(); } break; case AudioManager.AUDIOFOCUS_LOSS: Debug.logAudioFocus("Lost audio focus"); nativePause(STATE_PAUSED_AUDIOFOCUS); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: Debug.logAudioFocus("Lost audio focus (transient)"); nativePause(STATE_PAUSED_AUDIOFOCUS); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: //TODO deal with ducking? Debug.logAudioFocus("Lost audio focus (transient, can duck)"); nativePause(STATE_PAUSED_AUDIOFOCUS); break; } } private void onUnresponsiveAudio() { if (getState() != STATE_PLAYING) { Debug.logAudioResponsiveness("Is not playing when reacting to unresponsiveness, ignore."); return; } Debug.logAudioResponsiveness("Audio is unresponsive, skip track"); Debug.notifyAudioUnresponsive(mContext, "Audio unresponsive", "Skipping current track."); mQueue.next(); playTrack(); mHandler.removeCallbacks(mResondToUnresponsiveAudio); mHandler.postDelayed(mResondToUnresponsiveAudio, DURATION_BEFORE_AUDIO_UNRESPONSIVE_MS); } public void setTtsProvider(TtsProvider ttsProvider) { mTtsProvider = ttsProvider; } public interface Callback { void onStateChanged(int state); void onCurrentTrackUpdated(Track track); void onTrackProgress(int secondsPlayed, int secondsDuration); } }