1736 lines
57 KiB
Java
1736 lines
57 KiB
Java
package de.danoeh.antennapod.service.playback;
|
|
|
|
import java.io.IOException;
|
|
import java.util.List;
|
|
import java.util.concurrent.*;
|
|
|
|
import android.annotation.SuppressLint;
|
|
import android.app.Notification;
|
|
import android.app.PendingIntent;
|
|
import android.app.Service;
|
|
import android.content.BroadcastReceiver;
|
|
import android.content.ComponentName;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.IntentFilter;
|
|
import android.content.SharedPreferences;
|
|
import android.graphics.Bitmap;
|
|
import android.graphics.BitmapFactory;
|
|
import android.media.AudioManager;
|
|
import android.media.AudioManager.OnAudioFocusChangeListener;
|
|
import android.media.MediaMetadataRetriever;
|
|
import android.media.MediaPlayer;
|
|
import android.media.RemoteControlClient;
|
|
import android.media.RemoteControlClient.MetadataEditor;
|
|
import android.os.AsyncTask;
|
|
import android.os.Binder;
|
|
import android.os.IBinder;
|
|
import android.preference.PreferenceManager;
|
|
import android.support.v4.app.NotificationCompat;
|
|
import android.util.Log;
|
|
import android.view.KeyEvent;
|
|
import android.view.SurfaceHolder;
|
|
import de.danoeh.antennapod.AppConfig;
|
|
import de.danoeh.antennapod.R;
|
|
import de.danoeh.antennapod.activity.AudioplayerActivity;
|
|
import de.danoeh.antennapod.activity.VideoplayerActivity;
|
|
import de.danoeh.antennapod.feed.*;
|
|
import de.danoeh.antennapod.preferences.PlaybackPreferences;
|
|
import de.danoeh.antennapod.preferences.UserPreferences;
|
|
import de.danoeh.antennapod.receiver.MediaButtonReceiver;
|
|
import de.danoeh.antennapod.receiver.PlayerWidget;
|
|
import de.danoeh.antennapod.storage.DBReader;
|
|
import de.danoeh.antennapod.storage.DBTasks;
|
|
import de.danoeh.antennapod.storage.DBWriter;
|
|
import de.danoeh.antennapod.util.BitmapDecoder;
|
|
import de.danoeh.antennapod.util.QueueAccess;
|
|
import de.danoeh.antennapod.util.DuckType;
|
|
import de.danoeh.antennapod.util.flattr.FlattrUtils;
|
|
import de.danoeh.antennapod.util.playback.AudioPlayer;
|
|
import de.danoeh.antennapod.util.playback.IPlayer;
|
|
import de.danoeh.antennapod.util.playback.Playable;
|
|
import de.danoeh.antennapod.util.playback.Playable.PlayableException;
|
|
import de.danoeh.antennapod.util.playback.VideoPlayer;
|
|
import de.danoeh.antennapod.util.playback.PlaybackController;
|
|
|
|
/**
|
|
* Controls the MediaPlayer that plays a FeedMedia-file
|
|
*/
|
|
public class PlaybackService extends Service {
|
|
/**
|
|
* Logging tag
|
|
*/
|
|
private static final String TAG = "PlaybackService";
|
|
|
|
/**
|
|
* Parcelable of type Playable.
|
|
*/
|
|
public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra";
|
|
/**
|
|
* True if media should be streamed.
|
|
*/
|
|
public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.service.shouldStream";
|
|
/**
|
|
* True if playback should be started immediately after media has been
|
|
* prepared.
|
|
*/
|
|
public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.service.startWhenPrepared";
|
|
|
|
public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.service.prepareImmediately";
|
|
|
|
public static final String ACTION_PLAYER_STATUS_CHANGED = "action.de.danoeh.antennapod.service.playerStatusChanged";
|
|
private static final String AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged";
|
|
|
|
public static final String ACTION_PLAYER_NOTIFICATION = "action.de.danoeh.antennapod.service.playerNotification";
|
|
public static final String EXTRA_NOTIFICATION_CODE = "extra.de.danoeh.antennapod.service.notificationCode";
|
|
public static final String EXTRA_NOTIFICATION_TYPE = "extra.de.danoeh.antennapod.service.notificationType";
|
|
|
|
/**
|
|
* If the PlaybackService receives this action, it will stop playback and
|
|
* try to shutdown.
|
|
*/
|
|
public static final String ACTION_SHUTDOWN_PLAYBACK_SERVICE = "action.de.danoeh.antennapod.service.actionShutdownPlaybackService";
|
|
|
|
/**
|
|
* If the PlaybackService receives this action, it will end playback of the
|
|
* current episode and load the next episode if there is one available.
|
|
*/
|
|
public static final String ACTION_SKIP_CURRENT_EPISODE = "action.de.danoeh.antennapod.service.skipCurrentEpisode";
|
|
|
|
/**
|
|
* Used in NOTIFICATION_TYPE_RELOAD.
|
|
*/
|
|
public static final int EXTRA_CODE_AUDIO = 1;
|
|
public static final int EXTRA_CODE_VIDEO = 2;
|
|
|
|
public static final int NOTIFICATION_TYPE_ERROR = 0;
|
|
public static final int NOTIFICATION_TYPE_INFO = 1;
|
|
public static final int NOTIFICATION_TYPE_BUFFER_UPDATE = 2;
|
|
|
|
/**
|
|
* Receivers of this intent should update their information about the curently playing media
|
|
*/
|
|
public static final int NOTIFICATION_TYPE_RELOAD = 3;
|
|
/**
|
|
* The state of the sleeptimer changed.
|
|
*/
|
|
public static final int NOTIFICATION_TYPE_SLEEPTIMER_UPDATE = 4;
|
|
public static final int NOTIFICATION_TYPE_BUFFER_START = 5;
|
|
public static final int NOTIFICATION_TYPE_BUFFER_END = 6;
|
|
/**
|
|
* No more episodes are going to be played.
|
|
*/
|
|
public static final int NOTIFICATION_TYPE_PLAYBACK_END = 7;
|
|
|
|
/**
|
|
* Playback speed has changed
|
|
* */
|
|
public static final int NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE = 8;
|
|
|
|
/**
|
|
* Returned by getPositionSafe() or getDurationSafe() if the playbackService
|
|
* is in an invalid state.
|
|
*/
|
|
public static final int INVALID_TIME = -1;
|
|
|
|
/**
|
|
* Is true if service is running.
|
|
*/
|
|
public static boolean isRunning = false;
|
|
|
|
private static final int NOTIFICATION_ID = 1;
|
|
|
|
private volatile IPlayer player;
|
|
private RemoteControlClient remoteControlClient;
|
|
private AudioManager audioManager;
|
|
private ComponentName mediaButtonReceiver;
|
|
|
|
private volatile Playable media;
|
|
|
|
/**
|
|
* True if media should be streamed (Extracted from Intent Extra) .
|
|
*/
|
|
private boolean shouldStream;
|
|
|
|
private boolean startWhenPrepared;
|
|
private PlayerStatus status;
|
|
|
|
private PositionSaver positionSaver;
|
|
private ScheduledFuture positionSaverFuture;
|
|
|
|
private WidgetUpdateWorker widgetUpdater;
|
|
private ScheduledFuture widgetUpdaterFuture;
|
|
|
|
private SleepTimer sleepTimer;
|
|
private Future sleepTimerFuture;
|
|
|
|
private static final int SCHED_EX_POOL_SIZE = 3;
|
|
private ScheduledThreadPoolExecutor schedExecutor;
|
|
private ExecutorService dbLoaderExecutor;
|
|
|
|
private volatile PlayerStatus statusBeforeSeek;
|
|
|
|
private static boolean playingVideo;
|
|
|
|
/**
|
|
* True if mediaplayer was paused because it lost audio focus temporarily
|
|
*/
|
|
private boolean pausedBecauseOfTransientAudiofocusLoss;
|
|
|
|
private Thread chapterLoader;
|
|
|
|
private final IBinder mBinder = new LocalBinder();
|
|
|
|
private volatile List<FeedItem> queue;
|
|
|
|
public class LocalBinder extends Binder {
|
|
public PlaybackService getService() {
|
|
return PlaybackService.this;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean onUnbind(Intent intent) {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Received onUnbind event");
|
|
return super.onUnbind(intent);
|
|
}
|
|
|
|
/**
|
|
* Returns an intent which starts an audio- or videoplayer, depending on the
|
|
* type of media that is being played. If the playbackservice is not
|
|
* running, the type of the last played media will be looked up.
|
|
*/
|
|
public static Intent getPlayerActivityIntent(Context context) {
|
|
if (isRunning) {
|
|
if (playingVideo) {
|
|
return new Intent(context, VideoplayerActivity.class);
|
|
} else {
|
|
return new Intent(context, AudioplayerActivity.class);
|
|
}
|
|
} else {
|
|
if (PlaybackPreferences.getCurrentEpisodeIsVideo()) {
|
|
return new Intent(context, VideoplayerActivity.class);
|
|
} else {
|
|
return new Intent(context, AudioplayerActivity.class);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Same as getPlayerActivityIntent(context), but here the type of activity
|
|
* depends on the FeedMedia that is provided as an argument.
|
|
*/
|
|
public static Intent getPlayerActivityIntent(Context context, Playable media) {
|
|
MediaType mt = media.getMediaType();
|
|
if (mt == MediaType.VIDEO) {
|
|
return new Intent(context, VideoplayerActivity.class);
|
|
} else {
|
|
return new Intent(context, AudioplayerActivity.class);
|
|
}
|
|
}
|
|
|
|
@SuppressLint("NewApi")
|
|
@Override
|
|
public void onCreate() {
|
|
super.onCreate();
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Service created.");
|
|
isRunning = true;
|
|
pausedBecauseOfTransientAudiofocusLoss = false;
|
|
status = PlayerStatus.STOPPED;
|
|
audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
|
|
schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE,
|
|
new ThreadFactory() {
|
|
|
|
@Override
|
|
public Thread newThread(Runnable r) {
|
|
Thread t = new Thread(r);
|
|
t.setPriority(Thread.MIN_PRIORITY);
|
|
return t;
|
|
}
|
|
}, new RejectedExecutionHandler() {
|
|
|
|
@Override
|
|
public void rejectedExecution(Runnable r,
|
|
ThreadPoolExecutor executor) {
|
|
Log.w(TAG, "SchedEx rejected submission of new task");
|
|
}
|
|
}
|
|
);
|
|
dbLoaderExecutor = Executors.newSingleThreadExecutor();
|
|
|
|
mediaButtonReceiver = new ComponentName(getPackageName(),
|
|
MediaButtonReceiver.class.getName());
|
|
audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver);
|
|
if (android.os.Build.VERSION.SDK_INT >= 14) {
|
|
audioManager
|
|
.registerRemoteControlClient(setupRemoteControlClient());
|
|
}
|
|
registerReceiver(headsetDisconnected, new IntentFilter(
|
|
Intent.ACTION_HEADSET_PLUG));
|
|
registerReceiver(shutdownReceiver, new IntentFilter(
|
|
ACTION_SHUTDOWN_PLAYBACK_SERVICE));
|
|
registerReceiver(audioBecomingNoisy, new IntentFilter(
|
|
AudioManager.ACTION_AUDIO_BECOMING_NOISY));
|
|
registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter(
|
|
ACTION_SKIP_CURRENT_EPISODE));
|
|
EventDistributor.getInstance().register(eventDistributorListener);
|
|
loadQueue();
|
|
}
|
|
|
|
private IPlayer createMediaPlayer() {
|
|
if (player != null) {
|
|
player.release();
|
|
}
|
|
IPlayer player;
|
|
if (media == null || media.getMediaType() == MediaType.VIDEO) {
|
|
player = new VideoPlayer();
|
|
} else {
|
|
player = new AudioPlayer(this);
|
|
}
|
|
return createMediaPlayer(player);
|
|
}
|
|
|
|
private IPlayer createMediaPlayer(IPlayer mp) {
|
|
if (mp != null && media != null) {
|
|
if (media.getMediaType() == MediaType.AUDIO) {
|
|
((AudioPlayer) mp).setOnPreparedListener(audioPreparedListener);
|
|
((AudioPlayer) mp)
|
|
.setOnCompletionListener(audioCompletionListener);
|
|
((AudioPlayer) mp)
|
|
.setOnSeekCompleteListener(audioSeekCompleteListener);
|
|
((AudioPlayer) mp).setOnErrorListener(audioErrorListener);
|
|
((AudioPlayer) mp)
|
|
.setOnBufferingUpdateListener(audioBufferingUpdateListener);
|
|
((AudioPlayer) mp).setOnInfoListener(audioInfoListener);
|
|
} else {
|
|
((VideoPlayer) mp).setOnPreparedListener(videoPreparedListener);
|
|
((VideoPlayer) mp)
|
|
.setOnCompletionListener(videoCompletionListener);
|
|
((VideoPlayer) mp)
|
|
.setOnSeekCompleteListener(videoSeekCompleteListener);
|
|
((VideoPlayer) mp).setOnErrorListener(videoErrorListener);
|
|
((VideoPlayer) mp)
|
|
.setOnBufferingUpdateListener(videoBufferingUpdateListener);
|
|
((VideoPlayer) mp).setOnInfoListener(videoInfoListener);
|
|
}
|
|
}
|
|
return mp;
|
|
}
|
|
|
|
@SuppressLint("NewApi")
|
|
@Override
|
|
public void onDestroy() {
|
|
super.onDestroy();
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Service is about to be destroyed");
|
|
isRunning = false;
|
|
if (chapterLoader != null) {
|
|
chapterLoader.interrupt();
|
|
}
|
|
disableSleepTimer();
|
|
unregisterReceiver(headsetDisconnected);
|
|
unregisterReceiver(shutdownReceiver);
|
|
unregisterReceiver(audioBecomingNoisy);
|
|
unregisterReceiver(skipCurrentEpisodeReceiver);
|
|
EventDistributor.getInstance().unregister(eventDistributorListener);
|
|
if (android.os.Build.VERSION.SDK_INT >= 14) {
|
|
audioManager.unregisterRemoteControlClient(remoteControlClient);
|
|
}
|
|
audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver);
|
|
audioManager.abandonAudioFocus(audioFocusChangeListener);
|
|
player.release();
|
|
stopWidgetUpdater();
|
|
updateWidget();
|
|
}
|
|
|
|
@Override
|
|
public IBinder onBind(Intent intent) {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Received onBind event");
|
|
return mBinder;
|
|
}
|
|
|
|
private final EventDistributor.EventListener eventDistributorListener = new EventDistributor.EventListener() {
|
|
@Override
|
|
public void update(EventDistributor eventDistributor, Integer arg) {
|
|
if ((EventDistributor.QUEUE_UPDATE & arg) != 0) {
|
|
loadQueue();
|
|
}
|
|
}
|
|
};
|
|
|
|
private final OnAudioFocusChangeListener audioFocusChangeListener = new OnAudioFocusChangeListener() {
|
|
|
|
@Override
|
|
public void onAudioFocusChange(int focusChange) {
|
|
switch (focusChange) {
|
|
case AudioManager.AUDIOFOCUS_LOSS:
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Lost audio focus");
|
|
pause(true, false);
|
|
stopSelf();
|
|
break;
|
|
case AudioManager.AUDIOFOCUS_GAIN:
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Gained audio focus");
|
|
if (pausedBecauseOfTransientAudiofocusLoss) {
|
|
audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
|
|
AudioManager.ADJUST_RAISE, 0);
|
|
play();
|
|
}
|
|
break;
|
|
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
|
|
if (status == PlayerStatus.PLAYING) {
|
|
if (!UserPreferences.shouldPauseForFocusLoss()) {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Lost audio focus temporarily. Ducking...");
|
|
audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
|
|
AudioManager.ADJUST_LOWER, 0);
|
|
pausedBecauseOfTransientAudiofocusLoss = true;
|
|
} else {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Lost audio focus temporarily. Could duck, but won't, pausing...");
|
|
pause(false, false);
|
|
pausedBecauseOfTransientAudiofocusLoss = true;
|
|
}
|
|
}
|
|
break;
|
|
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
|
|
if (status == PlayerStatus.PLAYING) {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Lost audio focus temporarily. Pausing...");
|
|
pause(false, false);
|
|
pausedBecauseOfTransientAudiofocusLoss = true;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 1. Check type of intent
|
|
* 1.1 Keycode -> handle keycode -> done
|
|
* 1.2 Playable -> Step 2
|
|
* 2. Handle playable
|
|
* 2.1 Check current status
|
|
* 2.1.1 Not playing -> play new playable
|
|
* 2.1.2 Playing, new playable is the same -> play if playback is currently paused
|
|
* 2.1.3 Playing, new playable different -> Stop playback of old media
|
|
*
|
|
* @param intent
|
|
* @param flags
|
|
* @param startId
|
|
* @return
|
|
*/
|
|
@Override
|
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
|
super.onStartCommand(intent, flags, startId);
|
|
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "OnStartCommand called");
|
|
final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1);
|
|
final Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE);
|
|
if (keycode == -1 && playable == null) {
|
|
Log.e(TAG, "PlaybackService was started with no arguments");
|
|
stopSelf();
|
|
}
|
|
|
|
if (keycode != -1) {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Received media button event");
|
|
handleKeycode(keycode);
|
|
} else {
|
|
boolean playbackType = intent.getBooleanExtra(EXTRA_SHOULD_STREAM,
|
|
true);
|
|
if (media == null) {
|
|
media = playable;
|
|
shouldStream = playbackType;
|
|
startWhenPrepared = intent.getBooleanExtra(
|
|
EXTRA_START_WHEN_PREPARED, false);
|
|
initMediaplayer(intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false));
|
|
sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0);
|
|
}
|
|
if (media != null) {
|
|
if (!playable.getIdentifier().equals(media.getIdentifier())) {
|
|
// different media or different playback type
|
|
pause(true, false);
|
|
player.reset();
|
|
media = playable;
|
|
shouldStream = playbackType;
|
|
startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false);
|
|
initMediaplayer(intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false));
|
|
sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0);
|
|
} else {
|
|
// same media and same playback type
|
|
if (status == PlayerStatus.PAUSED) {
|
|
play();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return Service.START_NOT_STICKY;
|
|
}
|
|
|
|
/** Handles media button events */
|
|
private void handleKeycode(int keycode) {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Handling keycode: " + keycode);
|
|
switch (keycode) {
|
|
case KeyEvent.KEYCODE_HEADSETHOOK:
|
|
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
|
|
if (status == PlayerStatus.PLAYING) {
|
|
pause(true, true);
|
|
} else if (status == PlayerStatus.PAUSED) {
|
|
play();
|
|
} else if (status == PlayerStatus.PREPARING) {
|
|
setStartWhenPrepared(!startWhenPrepared);
|
|
} else if (status == PlayerStatus.INITIALIZED) {
|
|
startWhenPrepared = true;
|
|
prepare();
|
|
}
|
|
break;
|
|
case KeyEvent.KEYCODE_MEDIA_PLAY:
|
|
if (status == PlayerStatus.PAUSED) {
|
|
play();
|
|
} else if (status == PlayerStatus.INITIALIZED) {
|
|
startWhenPrepared = true;
|
|
prepare();
|
|
}
|
|
break;
|
|
case KeyEvent.KEYCODE_MEDIA_PAUSE:
|
|
if (status == PlayerStatus.PLAYING) {
|
|
pause(true, true);
|
|
}
|
|
break;
|
|
case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: {
|
|
seekDelta(PlaybackController.DEFAULT_SEEK_DELTA);
|
|
break;
|
|
}
|
|
case KeyEvent.KEYCODE_MEDIA_REWIND: {
|
|
seekDelta(-PlaybackController.DEFAULT_SEEK_DELTA);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called by a mediaplayer Activity as soon as it has prepared its
|
|
* mediaplayer.
|
|
*/
|
|
public void setVideoSurface(SurfaceHolder sh) {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Setting display");
|
|
player.setDisplay(null);
|
|
player.setDisplay(sh);
|
|
if (status == PlayerStatus.STOPPED
|
|
|| status == PlayerStatus.AWAITING_VIDEO_SURFACE) {
|
|
try {
|
|
InitTask initTask = new InitTask() {
|
|
|
|
@Override
|
|
protected void onPostExecute(Playable result) {
|
|
if (status == PlayerStatus.INITIALIZING) {
|
|
if (result != null) {
|
|
try {
|
|
if (shouldStream) {
|
|
player.setDataSource(media
|
|
.getStreamUrl());
|
|
setStatus(PlayerStatus.PREPARING);
|
|
player.prepareAsync();
|
|
} else {
|
|
player.setDataSource(media
|
|
.getLocalMediaUrl());
|
|
setStatus(PlayerStatus.PREPARING);
|
|
player.prepareAsync();
|
|
}
|
|
} catch (IOException e) {
|
|
e.printStackTrace();
|
|
}
|
|
} else {
|
|
setStatus(PlayerStatus.ERROR);
|
|
sendBroadcast(new Intent(
|
|
ACTION_SHUTDOWN_PLAYBACK_SERVICE));
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onPreExecute() {
|
|
setStatus(PlayerStatus.INITIALIZING);
|
|
}
|
|
|
|
};
|
|
initTask.executeAsync(media);
|
|
} catch (IllegalArgumentException e) {
|
|
e.printStackTrace();
|
|
} catch (SecurityException e) {
|
|
e.printStackTrace();
|
|
} catch (IllegalStateException e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Called when the surface holder of the mediaplayer has to be changed.
|
|
*/
|
|
private void resetVideoSurface() {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Resetting video surface");
|
|
cancelPositionSaver();
|
|
player.setDisplay(null);
|
|
player.reset();
|
|
player = createMediaPlayer();
|
|
status = PlayerStatus.STOPPED;
|
|
}
|
|
|
|
public void notifyVideoSurfaceAbandoned() {
|
|
resetVideoSurface();
|
|
if (media != null) {
|
|
initMediaplayer(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called after service has extracted the media it is supposed to play.
|
|
*
|
|
* @param prepareImmediately True if service should prepare playback after it has been initialized
|
|
*/
|
|
private void initMediaplayer(final boolean prepareImmediately) {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Setting up media player");
|
|
try {
|
|
MediaType mediaType = media.getMediaType();
|
|
player = createMediaPlayer();
|
|
if (mediaType == MediaType.AUDIO) {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Mime type is audio");
|
|
|
|
InitTask initTask = new InitTask() {
|
|
|
|
@Override
|
|
protected void onPostExecute(Playable result) {
|
|
// check if state of service has changed. If it has
|
|
// changed, assume that loaded metadata is not needed
|
|
// anymore.
|
|
if (status == PlayerStatus.INITIALIZING) {
|
|
if (result != null) {
|
|
playingVideo = false;
|
|
try {
|
|
if (shouldStream) {
|
|
player.setDataSource(media
|
|
.getStreamUrl());
|
|
} else if (media.localFileAvailable()) {
|
|
player.setDataSource(media
|
|
.getLocalMediaUrl());
|
|
}
|
|
|
|
if (prepareImmediately) {
|
|
setStatus(PlayerStatus.PREPARING);
|
|
player.prepareAsync();
|
|
} else {
|
|
setStatus(PlayerStatus.INITIALIZED);
|
|
}
|
|
} catch (IOException e) {
|
|
e.printStackTrace();
|
|
media = null;
|
|
setStatus(PlayerStatus.ERROR);
|
|
sendBroadcast(new Intent(
|
|
ACTION_SHUTDOWN_PLAYBACK_SERVICE));
|
|
}
|
|
} else {
|
|
Log.e(TAG, "InitTask could not load metadata");
|
|
media = null;
|
|
setStatus(PlayerStatus.ERROR);
|
|
sendBroadcast(new Intent(
|
|
ACTION_SHUTDOWN_PLAYBACK_SERVICE));
|
|
}
|
|
} else {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG,
|
|
"Status of player has changed during initialization. Stopping init process.");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onPreExecute() {
|
|
setStatus(PlayerStatus.INITIALIZING);
|
|
}
|
|
|
|
};
|
|
initTask.executeAsync(media);
|
|
} else if (mediaType == MediaType.VIDEO) {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Mime type is video");
|
|
playingVideo = true;
|
|
setStatus(PlayerStatus.AWAITING_VIDEO_SURFACE);
|
|
player.setScreenOnWhilePlaying(true);
|
|
}
|
|
|
|
} catch (IllegalArgumentException e) {
|
|
e.printStackTrace();
|
|
} catch (SecurityException e) {
|
|
e.printStackTrace();
|
|
} catch (IllegalStateException e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
|
|
private void setupPositionSaver() {
|
|
if (positionSaverFuture == null
|
|
|| (positionSaverFuture.isCancelled() || positionSaverFuture
|
|
.isDone())) {
|
|
|
|
positionSaver = new PositionSaver();
|
|
positionSaverFuture = schedExecutor.scheduleAtFixedRate(
|
|
positionSaver, PositionSaver.WAITING_INTERVALL,
|
|
PositionSaver.WAITING_INTERVALL, TimeUnit.MILLISECONDS);
|
|
}
|
|
}
|
|
|
|
private void cancelPositionSaver() {
|
|
if (positionSaverFuture != null) {
|
|
boolean result = positionSaverFuture.cancel(true);
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "PositionSaver cancelled. Result: " + result);
|
|
}
|
|
}
|
|
|
|
private final com.aocate.media.MediaPlayer.OnPreparedListener audioPreparedListener = new com.aocate.media.MediaPlayer.OnPreparedListener() {
|
|
@Override
|
|
public void onPrepared(com.aocate.media.MediaPlayer mp) {
|
|
genericOnPrepared(mp);
|
|
}
|
|
};
|
|
|
|
private final android.media.MediaPlayer.OnPreparedListener videoPreparedListener = new android.media.MediaPlayer.OnPreparedListener() {
|
|
@Override
|
|
public void onPrepared(android.media.MediaPlayer mp) {
|
|
genericOnPrepared(mp);
|
|
}
|
|
};
|
|
|
|
private final void genericOnPrepared(Object inObj) {
|
|
IPlayer mp = DuckType.coerce(inObj).to(IPlayer.class);
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Resource prepared");
|
|
mp.seekTo(media.getPosition());
|
|
if (media.getDuration() == 0) {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Setting duration of media");
|
|
media.setDuration(mp.getDuration());
|
|
}
|
|
setStatus(PlayerStatus.PREPARED);
|
|
if (chapterLoader != null) {
|
|
chapterLoader.interrupt();
|
|
}
|
|
chapterLoader = new Thread() {
|
|
@Override
|
|
public void run() {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Chapter loader started");
|
|
if (media != null && media.getChapters() == null) {
|
|
media.loadChapterMarks();
|
|
if (!isInterrupted() && media.getChapters() != null) {
|
|
sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD,
|
|
0);
|
|
}
|
|
}
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Chapter loader stopped");
|
|
}
|
|
};
|
|
chapterLoader.start();
|
|
|
|
if (startWhenPrepared) {
|
|
play();
|
|
}
|
|
}
|
|
|
|
private final com.aocate.media.MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener = new com.aocate.media.MediaPlayer.OnSeekCompleteListener() {
|
|
@Override
|
|
public void onSeekComplete(com.aocate.media.MediaPlayer mp) {
|
|
genericSeekCompleteListener();
|
|
}
|
|
};
|
|
|
|
private final android.media.MediaPlayer.OnSeekCompleteListener videoSeekCompleteListener = new android.media.MediaPlayer.OnSeekCompleteListener() {
|
|
@Override
|
|
public void onSeekComplete(android.media.MediaPlayer mp) {
|
|
genericSeekCompleteListener();
|
|
}
|
|
};
|
|
|
|
private final void genericSeekCompleteListener() {
|
|
if (status == PlayerStatus.SEEKING) {
|
|
setStatus(statusBeforeSeek);
|
|
}
|
|
}
|
|
|
|
private final com.aocate.media.MediaPlayer.OnInfoListener audioInfoListener = new com.aocate.media.MediaPlayer.OnInfoListener() {
|
|
@Override
|
|
public boolean onInfo(com.aocate.media.MediaPlayer mp, int what,
|
|
int extra) {
|
|
return genericInfoListener(what);
|
|
}
|
|
};
|
|
|
|
private final android.media.MediaPlayer.OnInfoListener videoInfoListener = new android.media.MediaPlayer.OnInfoListener() {
|
|
@Override
|
|
public boolean onInfo(android.media.MediaPlayer mp, int what, int extra) {
|
|
return genericInfoListener(what);
|
|
}
|
|
};
|
|
|
|
private boolean genericInfoListener(int what) {
|
|
switch (what) {
|
|
case MediaPlayer.MEDIA_INFO_BUFFERING_START:
|
|
sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_START, 0);
|
|
return true;
|
|
case MediaPlayer.MEDIA_INFO_BUFFERING_END:
|
|
sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_END, 0);
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private final com.aocate.media.MediaPlayer.OnErrorListener audioErrorListener = new com.aocate.media.MediaPlayer.OnErrorListener() {
|
|
@Override
|
|
public boolean onError(com.aocate.media.MediaPlayer mp, int what,
|
|
int extra) {
|
|
return genericOnError(mp, what, extra);
|
|
}
|
|
};
|
|
|
|
private final android.media.MediaPlayer.OnErrorListener videoErrorListener = new android.media.MediaPlayer.OnErrorListener() {
|
|
@Override
|
|
public boolean onError(android.media.MediaPlayer mp, int what, int extra) {
|
|
return genericOnError(mp, what, extra);
|
|
}
|
|
};
|
|
|
|
private boolean genericOnError(Object inObj, int what, int extra) {
|
|
final String TAG = "PlaybackService.onErrorListener";
|
|
Log.w(TAG, "An error has occured: " + what + " " + extra);
|
|
IPlayer mp = DuckType.coerce(inObj).to(IPlayer.class);
|
|
if (mp.isPlaying()) {
|
|
pause(true, true);
|
|
}
|
|
sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what);
|
|
setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING);
|
|
stopSelf();
|
|
return true;
|
|
}
|
|
|
|
private final com.aocate.media.MediaPlayer.OnCompletionListener audioCompletionListener = new com.aocate.media.MediaPlayer.OnCompletionListener() {
|
|
@Override
|
|
public void onCompletion(com.aocate.media.MediaPlayer mp) {
|
|
genericOnCompletion();
|
|
}
|
|
};
|
|
|
|
private final android.media.MediaPlayer.OnCompletionListener videoCompletionListener = new android.media.MediaPlayer.OnCompletionListener() {
|
|
@Override
|
|
public void onCompletion(android.media.MediaPlayer mp) {
|
|
genericOnCompletion();
|
|
}
|
|
};
|
|
|
|
private void genericOnCompletion() {
|
|
endPlayback(true);
|
|
}
|
|
|
|
private final com.aocate.media.MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener = new com.aocate.media.MediaPlayer.OnBufferingUpdateListener() {
|
|
@Override
|
|
public void onBufferingUpdate(com.aocate.media.MediaPlayer mp,
|
|
int percent) {
|
|
genericOnBufferingUpdate(percent);
|
|
}
|
|
};
|
|
|
|
private final android.media.MediaPlayer.OnBufferingUpdateListener videoBufferingUpdateListener = new android.media.MediaPlayer.OnBufferingUpdateListener() {
|
|
@Override
|
|
public void onBufferingUpdate(android.media.MediaPlayer mp, int percent) {
|
|
genericOnBufferingUpdate(percent);
|
|
}
|
|
};
|
|
|
|
private void genericOnBufferingUpdate(int percent) {
|
|
sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_UPDATE, percent);
|
|
}
|
|
|
|
private void endPlayback(boolean playNextEpisode) {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Playback ended");
|
|
audioManager.abandonAudioFocus(audioFocusChangeListener);
|
|
|
|
// Save state
|
|
cancelPositionSaver();
|
|
|
|
boolean isInQueue = false;
|
|
FeedItem nextItem = null;
|
|
|
|
if (media instanceof FeedMedia) {
|
|
FeedItem item = ((FeedMedia) media).getItem();
|
|
DBWriter.markItemRead(PlaybackService.this, item, true, true);
|
|
nextItem = DBTasks.getQueueSuccessorOfItem(this, item.getId(), queue);
|
|
isInQueue = media instanceof FeedMedia
|
|
&& QueueAccess.ItemListAccess(queue).contains(((FeedMedia) media).getItem().getId());
|
|
if (isInQueue) {
|
|
DBWriter.removeQueueItem(PlaybackService.this, item.getId(), true);
|
|
}
|
|
DBWriter.addItemToPlaybackHistory(PlaybackService.this, (FeedMedia) media);
|
|
long autoDeleteMediaId = ((FeedComponent) media).getId();
|
|
if (shouldStream) {
|
|
autoDeleteMediaId = -1;
|
|
}
|
|
}
|
|
|
|
// Load next episode if previous episode was in the queue and if there
|
|
// is an episode in the queue left.
|
|
// Start playback immediately if continuous playback is enabled
|
|
boolean loadNextItem = isInQueue && nextItem != null;
|
|
playNextEpisode = playNextEpisode && loadNextItem
|
|
&& UserPreferences.isFollowQueue();
|
|
if (loadNextItem) {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Loading next item in queue");
|
|
media = nextItem.getMedia();
|
|
}
|
|
final boolean prepareImmediately;
|
|
if (playNextEpisode) {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Playback of next episode will start immediately.");
|
|
prepareImmediately = startWhenPrepared = true;
|
|
} else {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "No more episodes available to play");
|
|
media = null;
|
|
prepareImmediately = startWhenPrepared = false;
|
|
stopForeground(true);
|
|
stopWidgetUpdater();
|
|
}
|
|
|
|
int notificationCode = 0;
|
|
if (media != null) {
|
|
shouldStream = !media.localFileAvailable();
|
|
if (media.getMediaType() == MediaType.AUDIO) {
|
|
notificationCode = EXTRA_CODE_AUDIO;
|
|
playingVideo = false;
|
|
} else if (media.getMediaType() == MediaType.VIDEO) {
|
|
notificationCode = EXTRA_CODE_VIDEO;
|
|
}
|
|
}
|
|
writePlaybackPreferences();
|
|
if (media != null) {
|
|
resetVideoSurface();
|
|
refreshRemoteControlClientState();
|
|
initMediaplayer(prepareImmediately);
|
|
|
|
sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD,
|
|
notificationCode);
|
|
} else {
|
|
sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0);
|
|
stopSelf();
|
|
}
|
|
}
|
|
|
|
public void setSleepTimer(long waitingTime) {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime)
|
|
+ " milliseconds");
|
|
if (sleepTimerFuture != null) {
|
|
sleepTimerFuture.cancel(true);
|
|
}
|
|
sleepTimer = new SleepTimer(waitingTime);
|
|
sleepTimerFuture = schedExecutor.submit(sleepTimer);
|
|
sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
|
|
}
|
|
|
|
public void disableSleepTimer() {
|
|
if (sleepTimerFuture != null) {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Disabling sleep timer");
|
|
sleepTimerFuture.cancel(true);
|
|
sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Saves the current position and pauses playback. Note that, if audiofocus
|
|
* is abandoned, the lockscreen controls will also disapear.
|
|
*
|
|
* @param abandonFocus
|
|
* is true if the service should release audio focus
|
|
* @param reinit
|
|
* is true if service should reinit after pausing if the media
|
|
* file is being streamed
|
|
*/
|
|
public void pause(boolean abandonFocus, boolean reinit) {
|
|
if (player.isPlaying()) {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Pausing playback.");
|
|
player.pause();
|
|
cancelPositionSaver();
|
|
saveCurrentPosition();
|
|
setStatus(PlayerStatus.PAUSED);
|
|
if (abandonFocus) {
|
|
audioManager.abandonAudioFocus(audioFocusChangeListener);
|
|
pausedBecauseOfTransientAudiofocusLoss = false;
|
|
disableSleepTimer();
|
|
}
|
|
stopWidgetUpdater();
|
|
stopForeground(true);
|
|
if (shouldStream && reinit) {
|
|
reinit();
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Pauses playback and destroys service. Recommended for video playback. */
|
|
public void stop() {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Stopping playback");
|
|
if (status == PlayerStatus.PREPARED || status == PlayerStatus.PAUSED
|
|
|| status == PlayerStatus.STOPPED
|
|
|| status == PlayerStatus.PLAYING) {
|
|
player.stop();
|
|
}
|
|
setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING);
|
|
stopSelf();
|
|
}
|
|
|
|
/**
|
|
* Prepared media player for playback if the service is in the INITALIZED
|
|
* state.
|
|
*/
|
|
public void prepare() {
|
|
if (status == PlayerStatus.INITIALIZED) {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Preparing media player");
|
|
setStatus(PlayerStatus.PREPARING);
|
|
player.prepareAsync();
|
|
}
|
|
}
|
|
|
|
/** Resets the media player and moves into INITIALIZED state. */
|
|
public void reinit() {
|
|
player.reset();
|
|
player = createMediaPlayer(player);
|
|
initMediaplayer(false);
|
|
}
|
|
|
|
@SuppressLint("NewApi")
|
|
public void play() {
|
|
if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED
|
|
|| status == PlayerStatus.STOPPED) {
|
|
int focusGained = audioManager.requestAudioFocus(
|
|
audioFocusChangeListener, AudioManager.STREAM_MUSIC,
|
|
AudioManager.AUDIOFOCUS_GAIN);
|
|
|
|
if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Audiofocus successfully requested");
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Resuming/Starting playback");
|
|
writePlaybackPreferences();
|
|
|
|
setSpeed(Float.parseFloat(UserPreferences.getPlaybackSpeed()));
|
|
player.start();
|
|
if (status != PlayerStatus.PAUSED) {
|
|
player.seekTo((int) media.getPosition());
|
|
}
|
|
setStatus(PlayerStatus.PLAYING);
|
|
setupPositionSaver();
|
|
setupWidgetUpdater();
|
|
setupNotification();
|
|
pausedBecauseOfTransientAudiofocusLoss = false;
|
|
if (android.os.Build.VERSION.SDK_INT >= 14) {
|
|
audioManager
|
|
.registerRemoteControlClient(remoteControlClient);
|
|
}
|
|
audioManager
|
|
.registerMediaButtonEventReceiver(mediaButtonReceiver);
|
|
media.onPlaybackStart();
|
|
} else {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Failed to request Audiofocus");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void writePlaybackPreferences() {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Writing playback preferences");
|
|
|
|
SharedPreferences.Editor editor = PreferenceManager
|
|
.getDefaultSharedPreferences(getApplicationContext()).edit();
|
|
if (media != null) {
|
|
editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA,
|
|
media.getPlayableType());
|
|
editor.putBoolean(
|
|
PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM,
|
|
shouldStream);
|
|
editor.putBoolean(
|
|
PlaybackPreferences.PREF_CURRENT_EPISODE_IS_VIDEO,
|
|
playingVideo);
|
|
if (media instanceof FeedMedia) {
|
|
FeedMedia fMedia = (FeedMedia) media;
|
|
editor.putLong(
|
|
PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
|
|
fMedia.getItem().getFeed().getId());
|
|
editor.putLong(
|
|
PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
|
|
fMedia.getId());
|
|
} else {
|
|
editor.putLong(
|
|
PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
|
|
PlaybackPreferences.NO_MEDIA_PLAYING);
|
|
editor.putLong(
|
|
PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
|
|
PlaybackPreferences.NO_MEDIA_PLAYING);
|
|
}
|
|
media.writeToPreferences(editor);
|
|
} else {
|
|
editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA,
|
|
PlaybackPreferences.NO_MEDIA_PLAYING);
|
|
editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
|
|
PlaybackPreferences.NO_MEDIA_PLAYING);
|
|
editor.putLong(
|
|
PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
|
|
PlaybackPreferences.NO_MEDIA_PLAYING);
|
|
}
|
|
|
|
editor.commit();
|
|
}
|
|
|
|
private void setStatus(PlayerStatus newStatus) {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Setting status to " + newStatus);
|
|
status = newStatus;
|
|
sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED));
|
|
updateWidget();
|
|
refreshRemoteControlClientState();
|
|
bluetoothNotifyChange();
|
|
}
|
|
|
|
/** Send ACTION_PLAYER_STATUS_CHANGED without changing the status attribute. */
|
|
private void postStatusUpdateIntent() {
|
|
setStatus(status);
|
|
}
|
|
|
|
private void sendNotificationBroadcast(int type, int code) {
|
|
Intent intent = new Intent(ACTION_PLAYER_NOTIFICATION);
|
|
intent.putExtra(EXTRA_NOTIFICATION_TYPE, type);
|
|
intent.putExtra(EXTRA_NOTIFICATION_CODE, code);
|
|
sendBroadcast(intent);
|
|
}
|
|
|
|
/** Used by setupNotification to load notification data in another thread. */
|
|
private AsyncTask<Void, Void, Void> notificationSetupTask;
|
|
|
|
/** Prepares notification and starts the service in the foreground. */
|
|
@SuppressLint("NewApi")
|
|
private void setupNotification() {
|
|
final PendingIntent pIntent = PendingIntent.getActivity(this, 0,
|
|
PlaybackService.getPlayerActivityIntent(this),
|
|
PendingIntent.FLAG_UPDATE_CURRENT);
|
|
|
|
if (notificationSetupTask != null) {
|
|
notificationSetupTask.cancel(true);
|
|
}
|
|
notificationSetupTask = new AsyncTask<Void, Void, Void>() {
|
|
Bitmap icon = null;
|
|
|
|
@Override
|
|
protected Void doInBackground(Void... params) {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Starting background work");
|
|
if (android.os.Build.VERSION.SDK_INT >= 11) {
|
|
if (media != null && media != null) {
|
|
int iconSize = getResources().getDimensionPixelSize(
|
|
android.R.dimen.notification_large_icon_width);
|
|
icon = BitmapDecoder
|
|
.decodeBitmapFromWorkerTaskResource(iconSize,
|
|
media);
|
|
}
|
|
|
|
}
|
|
if (icon == null) {
|
|
icon = BitmapFactory.decodeResource(getResources(),
|
|
R.drawable.ic_stat_antenna);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
protected void onPostExecute(Void result) {
|
|
super.onPostExecute(result);
|
|
if (!isCancelled() && status == PlayerStatus.PLAYING
|
|
&& media != null) {
|
|
String contentText = media.getFeedTitle();
|
|
String contentTitle = media.getEpisodeTitle();
|
|
Notification notification = null;
|
|
if (android.os.Build.VERSION.SDK_INT >= 16) {
|
|
Intent pauseButtonIntent = new Intent(
|
|
PlaybackService.this, PlaybackService.class);
|
|
pauseButtonIntent.putExtra(
|
|
MediaButtonReceiver.EXTRA_KEYCODE,
|
|
KeyEvent.KEYCODE_MEDIA_PAUSE);
|
|
PendingIntent pauseButtonPendingIntent = PendingIntent
|
|
.getService(PlaybackService.this, 0,
|
|
pauseButtonIntent,
|
|
PendingIntent.FLAG_UPDATE_CURRENT);
|
|
Notification.Builder notificationBuilder = new Notification.Builder(
|
|
PlaybackService.this)
|
|
.setContentTitle(contentTitle)
|
|
.setContentText(contentText)
|
|
.setOngoing(true)
|
|
.setContentIntent(pIntent)
|
|
.setLargeIcon(icon)
|
|
.setSmallIcon(R.drawable.ic_stat_antenna)
|
|
.addAction(android.R.drawable.ic_media_pause,
|
|
getString(R.string.pause_label),
|
|
pauseButtonPendingIntent);
|
|
notification = notificationBuilder.build();
|
|
} else {
|
|
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(
|
|
PlaybackService.this)
|
|
.setContentTitle(contentTitle)
|
|
.setContentText(contentText).setOngoing(true)
|
|
.setContentIntent(pIntent).setLargeIcon(icon)
|
|
.setSmallIcon(R.drawable.ic_stat_antenna);
|
|
notification = notificationBuilder.getNotification();
|
|
}
|
|
startForeground(NOTIFICATION_ID, notification);
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Notification set up");
|
|
}
|
|
}
|
|
|
|
};
|
|
if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
|
|
notificationSetupTask
|
|
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
|
} else {
|
|
notificationSetupTask.execute();
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Seek a specific position from the current position
|
|
*
|
|
* @param delta
|
|
* offset from current position (positive or negative)
|
|
* */
|
|
public void seekDelta(int delta) {
|
|
int position = getCurrentPositionSafe();
|
|
if (position != INVALID_TIME) {
|
|
seek(player.getCurrentPosition() + delta);
|
|
}
|
|
}
|
|
|
|
public void seek(int i) {
|
|
saveCurrentPosition();
|
|
if (status == PlayerStatus.INITIALIZED
|
|
|| status == PlayerStatus.INITIALIZING
|
|
|| status == PlayerStatus.PREPARING) {
|
|
media.setPosition(i);
|
|
setStartWhenPrepared(true);
|
|
prepare();
|
|
} else {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Seeking position " + i);
|
|
if (shouldStream) {
|
|
if (status != PlayerStatus.SEEKING) {
|
|
statusBeforeSeek = status;
|
|
}
|
|
setStatus(PlayerStatus.SEEKING);
|
|
}
|
|
player.seekTo(i);
|
|
}
|
|
}
|
|
|
|
public void seekToChapter(Chapter chapter) {
|
|
seek((int) chapter.getStart());
|
|
}
|
|
|
|
/** Saves the current position of the media file to the DB */
|
|
private synchronized void saveCurrentPosition() {
|
|
int position = getCurrentPositionSafe();
|
|
if (position != INVALID_TIME) {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Saving current position to " + position);
|
|
media.saveCurrentPosition(PreferenceManager
|
|
.getDefaultSharedPreferences(getApplicationContext()),
|
|
position);
|
|
}
|
|
}
|
|
|
|
private void stopWidgetUpdater() {
|
|
if (widgetUpdaterFuture != null) {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Stopping widgetUpdateWorker");
|
|
widgetUpdaterFuture.cancel(true);
|
|
}
|
|
sendBroadcast(new Intent(PlayerWidget.STOP_WIDGET_UPDATE));
|
|
}
|
|
|
|
@SuppressLint("NewApi")
|
|
private void setupWidgetUpdater() {
|
|
if (widgetUpdaterFuture == null
|
|
|| (widgetUpdaterFuture.isCancelled() || widgetUpdaterFuture
|
|
.isDone())) {
|
|
widgetUpdater = new WidgetUpdateWorker();
|
|
widgetUpdaterFuture = schedExecutor.scheduleAtFixedRate(
|
|
widgetUpdater, WidgetUpdateWorker.NOTIFICATION_INTERVALL,
|
|
WidgetUpdateWorker.NOTIFICATION_INTERVALL,
|
|
TimeUnit.MILLISECONDS);
|
|
}
|
|
}
|
|
|
|
private void updateWidget() {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Sending widget update request");
|
|
PlaybackService.this.sendBroadcast(new Intent(
|
|
PlayerWidget.FORCE_WIDGET_UPDATE));
|
|
}
|
|
|
|
public boolean sleepTimerActive() {
|
|
return sleepTimer != null && sleepTimer.isWaiting();
|
|
}
|
|
|
|
public long getSleepTimerTimeLeft() {
|
|
if (sleepTimerActive()) {
|
|
return sleepTimer.getWaitingTime();
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
@SuppressLint("NewApi")
|
|
private RemoteControlClient setupRemoteControlClient() {
|
|
Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
|
|
mediaButtonIntent.setComponent(mediaButtonReceiver);
|
|
PendingIntent mediaPendingIntent = PendingIntent.getBroadcast(
|
|
getApplicationContext(), 0, mediaButtonIntent, 0);
|
|
remoteControlClient = new RemoteControlClient(mediaPendingIntent);
|
|
int controlFlags;
|
|
if (android.os.Build.VERSION.SDK_INT < 16) {
|
|
controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE
|
|
| RemoteControlClient.FLAG_KEY_MEDIA_NEXT;
|
|
} else {
|
|
controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE;
|
|
}
|
|
remoteControlClient.setTransportControlFlags(controlFlags);
|
|
return remoteControlClient;
|
|
}
|
|
|
|
/** Refresh player status and metadata. */
|
|
@SuppressLint("NewApi")
|
|
private void refreshRemoteControlClientState() {
|
|
if (android.os.Build.VERSION.SDK_INT >= 14) {
|
|
if (remoteControlClient != null) {
|
|
switch (status) {
|
|
case PLAYING:
|
|
remoteControlClient
|
|
.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING);
|
|
break;
|
|
case PAUSED:
|
|
case INITIALIZED:
|
|
remoteControlClient
|
|
.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED);
|
|
break;
|
|
case STOPPED:
|
|
remoteControlClient
|
|
.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED);
|
|
break;
|
|
case ERROR:
|
|
remoteControlClient
|
|
.setPlaybackState(RemoteControlClient.PLAYSTATE_ERROR);
|
|
break;
|
|
default:
|
|
remoteControlClient
|
|
.setPlaybackState(RemoteControlClient.PLAYSTATE_BUFFERING);
|
|
}
|
|
if (media != null) {
|
|
MetadataEditor editor = remoteControlClient
|
|
.editMetadata(false);
|
|
editor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE,
|
|
media.getEpisodeTitle());
|
|
|
|
editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM,
|
|
media.getFeedTitle());
|
|
|
|
editor.apply();
|
|
}
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "RemoteControlClient state was refreshed");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void bluetoothNotifyChange() {
|
|
boolean isPlaying = false;
|
|
|
|
if (status == PlayerStatus.PLAYING) {
|
|
isPlaying = true;
|
|
}
|
|
|
|
if (media != null) {
|
|
Intent i = new Intent(AVRCP_ACTION_PLAYER_STATUS_CHANGED);
|
|
i.putExtra("id", 1);
|
|
i.putExtra("artist", "");
|
|
i.putExtra("album", media.getFeedTitle());
|
|
i.putExtra("track", media.getEpisodeTitle());
|
|
i.putExtra("playing", isPlaying);
|
|
if (queue != null) {
|
|
i.putExtra("ListSize", queue.size());
|
|
}
|
|
i.putExtra("duration", media.getDuration());
|
|
i.putExtra("position", media.getPosition());
|
|
sendBroadcast(i);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pauses playback when the headset is disconnected and the preference is
|
|
* set
|
|
*/
|
|
private BroadcastReceiver headsetDisconnected = new BroadcastReceiver() {
|
|
private static final String TAG = "headsetDisconnected";
|
|
private static final int UNPLUGGED = 0;
|
|
|
|
@Override
|
|
public void onReceive(Context context, Intent intent) {
|
|
if (intent.getAction().equals(Intent.ACTION_HEADSET_PLUG)) {
|
|
int state = intent.getIntExtra("state", -1);
|
|
if (state != -1) {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Headset plug event. State is " + state);
|
|
if (state == UNPLUGGED && status == PlayerStatus.PLAYING) {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Headset was unplugged during playback.");
|
|
pauseIfPauseOnDisconnect();
|
|
}
|
|
} else {
|
|
Log.e(TAG, "Received invalid ACTION_HEADSET_PLUG intent");
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
private BroadcastReceiver audioBecomingNoisy = new BroadcastReceiver() {
|
|
|
|
@Override
|
|
public void onReceive(Context context, Intent intent) {
|
|
// sound is about to change, eg. bluetooth -> speaker
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Pausing playback because audio is becoming noisy");
|
|
pauseIfPauseOnDisconnect();
|
|
}
|
|
// android.media.AUDIO_BECOMING_NOISY
|
|
};
|
|
|
|
/** Pauses playback if PREF_PAUSE_ON_HEADSET_DISCONNECT was set to true. */
|
|
private void pauseIfPauseOnDisconnect() {
|
|
if (UserPreferences.isPauseOnHeadsetDisconnect()
|
|
&& status == PlayerStatus.PLAYING) {
|
|
pause(true, true);
|
|
}
|
|
}
|
|
|
|
private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() {
|
|
|
|
@Override
|
|
public void onReceive(Context context, Intent intent) {
|
|
if (intent.getAction().equals(ACTION_SHUTDOWN_PLAYBACK_SERVICE)) {
|
|
schedExecutor.shutdownNow();
|
|
stop();
|
|
media = null;
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
private BroadcastReceiver skipCurrentEpisodeReceiver = new BroadcastReceiver() {
|
|
@Override
|
|
public void onReceive(Context context, Intent intent) {
|
|
if (intent.getAction().equals(ACTION_SKIP_CURRENT_EPISODE)) {
|
|
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent");
|
|
if (media != null) {
|
|
setStatus(PlayerStatus.STOPPED);
|
|
endPlayback(true);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/** Periodically saves the position of the media file */
|
|
class PositionSaver implements Runnable {
|
|
public static final int WAITING_INTERVALL = 5000;
|
|
|
|
@Override
|
|
public void run() {
|
|
if (player != null && player.isPlaying()) {
|
|
try {
|
|
saveCurrentPosition();
|
|
} catch (IllegalStateException e) {
|
|
Log.w(TAG,
|
|
"saveCurrentPosition was called in illegal state");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Notifies the player widget in the specified intervall */
|
|
class WidgetUpdateWorker implements Runnable {
|
|
private static final int NOTIFICATION_INTERVALL = 1000;
|
|
|
|
@Override
|
|
public void run() {
|
|
if (PlaybackService.isRunning) {
|
|
updateWidget();
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Sleeps for a given time and then pauses playback. */
|
|
class SleepTimer implements Runnable {
|
|
private static final String TAG = "SleepTimer";
|
|
private static final long UPDATE_INTERVALL = 1000L;
|
|
private volatile long waitingTime;
|
|
private boolean isWaiting;
|
|
|
|
public SleepTimer(long waitingTime) {
|
|
super();
|
|
this.waitingTime = waitingTime;
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
isWaiting = true;
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Starting");
|
|
while (waitingTime > 0) {
|
|
try {
|
|
Thread.sleep(UPDATE_INTERVALL);
|
|
waitingTime -= UPDATE_INTERVALL;
|
|
|
|
if (waitingTime <= 0) {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Waiting completed");
|
|
if (status == PlayerStatus.PLAYING) {
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Pausing playback");
|
|
pause(true, true);
|
|
}
|
|
postExecute();
|
|
}
|
|
} catch (InterruptedException e) {
|
|
Log.d(TAG, "Thread was interrupted while waiting");
|
|
break;
|
|
}
|
|
}
|
|
postExecute();
|
|
}
|
|
|
|
protected void postExecute() {
|
|
isWaiting = false;
|
|
sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
|
|
}
|
|
|
|
public long getWaitingTime() {
|
|
return waitingTime;
|
|
}
|
|
|
|
public boolean isWaiting() {
|
|
return isWaiting;
|
|
}
|
|
|
|
}
|
|
|
|
public static boolean isPlayingVideo() {
|
|
return playingVideo;
|
|
}
|
|
|
|
public boolean isShouldStream() {
|
|
return shouldStream;
|
|
}
|
|
|
|
public PlayerStatus getStatus() {
|
|
return status;
|
|
}
|
|
|
|
public Playable getMedia() {
|
|
return media;
|
|
}
|
|
|
|
public IPlayer getPlayer() {
|
|
return player;
|
|
}
|
|
|
|
public boolean isStartWhenPrepared() {
|
|
return startWhenPrepared;
|
|
}
|
|
|
|
public void setStartWhenPrepared(boolean startWhenPrepared) {
|
|
this.startWhenPrepared = startWhenPrepared;
|
|
postStatusUpdateIntent();
|
|
}
|
|
|
|
public boolean canSetSpeed() {
|
|
if (player != null && media != null && media.getMediaType() == MediaType.AUDIO) {
|
|
return ((AudioPlayer) player).canSetSpeed();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public boolean canSetPitch() {
|
|
if (player != null && media != null && media.getMediaType() == MediaType.AUDIO) {
|
|
return ((AudioPlayer) player).canSetPitch();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public void setSpeed(float speed) {
|
|
if (media != null && media.getMediaType() == MediaType.AUDIO) {
|
|
AudioPlayer audioPlayer = (AudioPlayer) player;
|
|
if (audioPlayer.canSetSpeed()) {
|
|
audioPlayer.setPlaybackSpeed((float) speed);
|
|
if (AppConfig.DEBUG)
|
|
Log.d(TAG, "Playback speed was set to " + speed);
|
|
sendNotificationBroadcast(
|
|
NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void setPitch(float pitch) {
|
|
if (media != null && media.getMediaType() == MediaType.AUDIO) {
|
|
AudioPlayer audioPlayer = (AudioPlayer) player;
|
|
if (audioPlayer.canSetPitch()) {
|
|
audioPlayer.setPlaybackPitch((float) pitch);
|
|
}
|
|
}
|
|
}
|
|
|
|
public float getCurrentPlaybackSpeed() {
|
|
if (media.getMediaType() == MediaType.AUDIO
|
|
&& player instanceof AudioPlayer) {
|
|
AudioPlayer audioPlayer = (AudioPlayer) player;
|
|
if (audioPlayer.canSetSpeed()) {
|
|
return audioPlayer.getCurrentSpeedMultiplier();
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* call getDuration() on mediaplayer or return INVALID_TIME if player is in
|
|
* an invalid state. This method should be used instead of calling
|
|
* getDuration() directly to avoid an error.
|
|
*/
|
|
public int getDurationSafe() {
|
|
if (status != null && player != null) {
|
|
switch (status) {
|
|
case PREPARED:
|
|
case PLAYING:
|
|
case PAUSED:
|
|
case SEEKING:
|
|
try {
|
|
return player.getDuration();
|
|
} catch (IllegalStateException e) {
|
|
e.printStackTrace();
|
|
return INVALID_TIME;
|
|
}
|
|
default:
|
|
return INVALID_TIME;
|
|
}
|
|
} else {
|
|
return INVALID_TIME;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* call getCurrentPosition() on mediaplayer or return INVALID_TIME if player
|
|
* is in an invalid state. This method should be used instead of calling
|
|
* getCurrentPosition() directly to avoid an error.
|
|
*/
|
|
public int getCurrentPositionSafe() {
|
|
if (status != null && player != null) {
|
|
switch (status) {
|
|
case PREPARED:
|
|
case PLAYING:
|
|
case PAUSED:
|
|
case SEEKING:
|
|
return player.getCurrentPosition();
|
|
default:
|
|
return INVALID_TIME;
|
|
}
|
|
} else {
|
|
return INVALID_TIME;
|
|
}
|
|
}
|
|
|
|
private void setCurrentlyPlayingMedia(long id) {
|
|
SharedPreferences.Editor editor = PreferenceManager
|
|
.getDefaultSharedPreferences(getApplicationContext()).edit();
|
|
editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, id);
|
|
editor.commit();
|
|
}
|
|
|
|
private static class InitTask extends AsyncTask<Playable, Void, Playable> {
|
|
private Playable playable;
|
|
public PlayableException exception;
|
|
|
|
@Override
|
|
protected Playable doInBackground(Playable... params) {
|
|
if (params[0] == null) {
|
|
throw new IllegalArgumentException("Playable must not be null");
|
|
}
|
|
playable = params[0];
|
|
|
|
try {
|
|
playable.loadMetadata();
|
|
} catch (PlayableException e) {
|
|
e.printStackTrace();
|
|
exception = e;
|
|
return null;
|
|
}
|
|
return playable;
|
|
}
|
|
|
|
@SuppressLint("NewApi")
|
|
public void executeAsync(Playable playable) {
|
|
FlattrUtils.hasToken();
|
|
if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
|
|
executeOnExecutor(THREAD_POOL_EXECUTOR, playable);
|
|
} else {
|
|
execute(playable);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private void loadQueue() {
|
|
dbLoaderExecutor.submit(new QueueLoaderTask());
|
|
}
|
|
|
|
private class QueueLoaderTask implements Runnable {
|
|
@Override
|
|
public void run() {
|
|
List<FeedItem> queueRef = DBReader.getQueue(PlaybackService.this);
|
|
queue = queueRef;
|
|
}
|
|
}
|
|
}
|