summaryrefslogtreecommitdiff
path: root/src/de/danoeh/antennapod/service
diff options
context:
space:
mode:
authordaniel oeh <daniel.oeh@gmail.com>2014-02-11 18:58:08 +0100
committerdaniel oeh <daniel.oeh@gmail.com>2014-02-11 18:58:08 +0100
commit5707d014d0f02055512f05fed1b9f07c45993f8d (patch)
tree57c408d6d12b89d785981bd6902cdfccc5df1623 /src/de/danoeh/antennapod/service
parentbad86d8284fb48bd130370eefb8d8c7ae25ab44c (diff)
parent8e05212c750e9bccee0681efc40d75eebd16fb78 (diff)
downloadAntennaPod-5707d014d0f02055512f05fed1b9f07c45993f8d.zip
Merge branch 'playbackservice_rewrite' into develop
Diffstat (limited to 'src/de/danoeh/antennapod/service')
-rw-r--r--src/de/danoeh/antennapod/service/PlaybackService.java1736
-rw-r--r--src/de/danoeh/antennapod/service/playback/PlaybackService.java1006
-rw-r--r--src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java917
-rw-r--r--src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java384
-rw-r--r--src/de/danoeh/antennapod/service/playback/PlayerStatus.java (renamed from src/de/danoeh/antennapod/service/PlayerStatus.java)6
-rw-r--r--src/de/danoeh/antennapod/service/playback/PlayerWidgetService.java (renamed from src/de/danoeh/antennapod/service/PlayerWidgetService.java)18
6 files changed, 2319 insertions, 1748 deletions
diff --git a/src/de/danoeh/antennapod/service/PlaybackService.java b/src/de/danoeh/antennapod/service/PlaybackService.java
deleted file mode 100644
index 2fa4e10d9..000000000
--- a/src/de/danoeh/antennapod/service/PlaybackService.java
+++ /dev/null
@@ -1,1736 +0,0 @@
-package de.danoeh.antennapod.service;
-
-import java.io.IOException;
-import java.util.Date;
-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;
- }
- }
-}
diff --git a/src/de/danoeh/antennapod/service/playback/PlaybackService.java b/src/de/danoeh/antennapod/service/playback/PlaybackService.java
new file mode 100644
index 000000000..21aca915e
--- /dev/null
+++ b/src/de/danoeh/antennapod/service/playback/PlaybackService.java
@@ -0,0 +1,1006 @@
+package de.danoeh.antennapod.service.playback;
+
+import android.annotation.SuppressLint;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.*;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.AudioManager;
+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.Build;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
+import android.support.v4.app.NotificationCompat;
+import android.util.Log;
+import android.util.Pair;
+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.Chapter;
+import de.danoeh.antennapod.feed.FeedItem;
+import de.danoeh.antennapod.feed.FeedMedia;
+import de.danoeh.antennapod.feed.MediaType;
+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.DBTasks;
+import de.danoeh.antennapod.storage.DBWriter;
+import de.danoeh.antennapod.util.BitmapDecoder;
+import de.danoeh.antennapod.util.QueueAccess;
+import de.danoeh.antennapod.util.playback.Playable;
+import de.danoeh.antennapod.util.playback.PlaybackController;
+
+import java.util.List;
+
+/**
+ * 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 RemoteControlClient remoteControlClient;
+ private PlaybackServiceMediaPlayer mediaPlayer;
+ private PlaybackServiceTaskManager taskManager;
+
+ private static volatile MediaType currentMediaType = MediaType.UNKNOWN;
+
+ private final IBinder mBinder = new LocalBinder();
+
+ 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 (currentMediaType == MediaType.VIDEO) {
+ 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;
+
+ 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));
+ remoteControlClient = setupRemoteControlClient();
+ taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback);
+ mediaPlayer = new PlaybackServiceMediaPlayer(this, mediaPlayerCallback);
+
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Service is about to be destroyed");
+ isRunning = false;
+ currentMediaType = MediaType.UNKNOWN;
+
+ unregisterReceiver(headsetDisconnected);
+ unregisterReceiver(shutdownReceiver);
+ unregisterReceiver(audioBecomingNoisy);
+ unregisterReceiver(skipCurrentEpisodeReceiver);
+ mediaPlayer.shutdown();
+ taskManager.shutdown();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Received onBind event");
+ return mBinder;
+ }
+
+ @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 ((flags & Service.START_FLAG_REDELIVERY) != 0) {
+ if (AppConfig.DEBUG) Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now.");
+ stopForeground(true);
+ }
+
+ if (keycode != -1) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Received media button event");
+ handleKeycode(keycode);
+ } else {
+ boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM,
+ true);
+ boolean startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false);
+ boolean prepareImmediately = intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false);
+ sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0);
+ mediaPlayer.playMediaObject(playable, stream, startWhenPrepared, prepareImmediately);
+ }
+
+ return Service.START_REDELIVER_INTENT;
+ }
+
+ /**
+ * Handles media button events
+ */
+ private void handleKeycode(int keycode) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Handling keycode: " + keycode);
+
+ final PlayerStatus status = mediaPlayer.getPSMPInfo().playerStatus;
+ switch (keycode) {
+ case KeyEvent.KEYCODE_HEADSETHOOK:
+ case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+ if (status == PlayerStatus.PLAYING) {
+ mediaPlayer.pause(true, true);
+ } else if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) {
+ mediaPlayer.resume();
+ } else if (status == PlayerStatus.PREPARING) {
+ mediaPlayer.setStartWhenPrepared(!mediaPlayer.isStartWhenPrepared());
+ } else if (status == PlayerStatus.INITIALIZED) {
+ mediaPlayer.setStartWhenPrepared(true);
+ mediaPlayer.prepare();
+ }
+ break;
+ case KeyEvent.KEYCODE_MEDIA_PLAY:
+ if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) {
+ mediaPlayer.resume();
+ } else if (status == PlayerStatus.INITIALIZED) {
+ mediaPlayer.setStartWhenPrepared(true);
+ mediaPlayer.prepare();
+ }
+ break;
+ case KeyEvent.KEYCODE_MEDIA_PAUSE:
+ if (status == PlayerStatus.PLAYING) {
+ mediaPlayer.pause(true, true);
+ }
+ break;
+ case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: {
+ mediaPlayer.seekDelta(PlaybackController.DEFAULT_SEEK_DELTA);
+ break;
+ }
+ case KeyEvent.KEYCODE_MEDIA_REWIND: {
+ mediaPlayer.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");
+ mediaPlayer.setVideoSurface(sh);
+ }
+
+ /**
+ * Called when the surface holder of the mediaplayer has to be changed.
+ */
+ private void resetVideoSurface() {
+ taskManager.cancelPositionSaver();
+ mediaPlayer.resetVideoSurface();
+ }
+
+ public void notifyVideoSurfaceAbandoned() {
+ stopForeground(true);
+ mediaPlayer.resetVideoSurface();
+ }
+
+ private final PlaybackServiceTaskManager.PSTMCallback taskManagerCallback = new PlaybackServiceTaskManager.PSTMCallback() {
+ @Override
+ public void positionSaverTick() {
+ saveCurrentPosition();
+ }
+
+ @Override
+ public void onSleepTimerExpired() {
+ mediaPlayer.pause(true, true);
+ sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
+ }
+
+
+ @Override
+ public void onWidgetUpdaterTick() {
+ updateWidget();
+ }
+
+ @Override
+ public void onChapterLoaded(Playable media) {
+ sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0);
+ }
+ };
+
+ private final PlaybackServiceMediaPlayer.PSMPCallback mediaPlayerCallback = new PlaybackServiceMediaPlayer.PSMPCallback() {
+ @Override
+ public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ currentMediaType = mediaPlayer.getCurrentMediaType();
+ switch (newInfo.playerStatus) {
+ case INITIALIZED:
+ writePlaybackPreferences();
+ break;
+
+ case PREPARED:
+ taskManager.startChapterLoader(newInfo.playable);
+ break;
+
+ case PAUSED:
+ taskManager.cancelPositionSaver();
+ saveCurrentPosition();
+ taskManager.cancelWidgetUpdater();
+ stopForeground(true);
+ break;
+
+ case STOPPED:
+ //setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING);
+ //stopSelf();
+ break;
+
+ case PLAYING:
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Audiofocus successfully requested");
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Resuming/Starting playback");
+
+ taskManager.startPositionSaver();
+ taskManager.startWidgetUpdater();
+ setupNotification(newInfo);
+ break;
+
+ }
+
+ sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED));
+ updateWidget();
+ refreshRemoteControlClientState(newInfo);
+ bluetoothNotifyChange(newInfo);
+ }
+
+ @Override
+ public void shouldStop() {
+ stopSelf();
+ }
+
+ @Override
+ public void playbackSpeedChanged(float s) {
+ sendNotificationBroadcast(
+ NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE, 0);
+ }
+
+ @Override
+ public void onBufferingUpdate(int percent) {
+ sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_UPDATE, percent);
+ }
+
+ @Override
+ public boolean onMediaPlayerInfo(int code) {
+ switch (code) {
+ 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;
+ }
+ }
+
+ @Override
+ public boolean onMediaPlayerError(Object inObj, int what, int extra) {
+ final String TAG = "PlaybackService.onErrorListener";
+ Log.w(TAG, "An error has occured: " + what + " " + extra);
+ if (mediaPlayer.getPSMPInfo().playerStatus == PlayerStatus.PLAYING) {
+ mediaPlayer.pause(true, false);
+ }
+ sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what);
+ setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING);
+ stopSelf();
+ return true;
+ }
+
+ @Override
+ public boolean endPlayback(boolean playNextEpisode) {
+ PlaybackService.this.endPlayback(true);
+ return true;
+ }
+
+ @Override
+ public RemoteControlClient getRemoteControlClient() {
+ return remoteControlClient;
+ }
+ };
+
+ private void endPlayback(boolean playNextEpisode) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Playback ended");
+
+ final Playable media = mediaPlayer.getPSMPInfo().playable;
+ if (media == null) {
+ Log.e(TAG, "Cannot end playback: media was null");
+ return;
+ }
+
+ taskManager.cancelPositionSaver();
+
+ boolean isInQueue = false;
+ FeedItem nextItem = null;
+
+ if (media instanceof FeedMedia) {
+ FeedItem item = ((FeedMedia) media).getItem();
+ DBWriter.markItemRead(PlaybackService.this, item, true, true);
+
+ try {
+ final List<FeedItem> queue = taskManager.getQueue();
+ isInQueue = QueueAccess.ItemListAccess(queue).contains(((FeedMedia) media).getItem().getId());
+ nextItem = DBTasks.getQueueSuccessorOfItem(this, item.getId(), queue);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ // isInQueue remains false
+ }
+ if (isInQueue) {
+ DBWriter.removeQueueItem(PlaybackService.this, item.getId(), true);
+ }
+ DBWriter.addItemToPlaybackHistory(PlaybackService.this, (FeedMedia) media);
+ }
+
+ // 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
+ Playable nextMedia = null;
+ boolean loadNextItem = isInQueue && nextItem != null;
+ playNextEpisode = playNextEpisode && loadNextItem
+ && UserPreferences.isFollowQueue();
+ if (loadNextItem) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Loading next item in queue");
+ nextMedia = nextItem.getMedia();
+ }
+ final boolean prepareImmediately;
+ final boolean startWhenPrepared;
+ final boolean stream;
+
+ 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");
+
+ prepareImmediately = startWhenPrepared = false;
+ stopForeground(true);
+ stopWidgetUpdater();
+ }
+
+ writePlaybackPreferences();
+ if (nextMedia != null) {
+ stream = !media.localFileAvailable();
+ mediaPlayer.playMediaObject(nextMedia, stream, startWhenPrepared, prepareImmediately);
+ sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD,
+ (nextMedia.getMediaType() == MediaType.VIDEO) ? EXTRA_CODE_VIDEO : EXTRA_CODE_AUDIO);
+ } 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");
+ taskManager.setSleepTimer(waitingTime);
+ sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
+ }
+
+ public void disableSleepTimer() {
+ taskManager.disableSleepTimer();
+ sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
+ }
+
+
+ private void writePlaybackPreferences() {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Writing playback preferences");
+
+ SharedPreferences.Editor editor = PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext()).edit();
+ PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo();
+ MediaType mediaType = mediaPlayer.getCurrentMediaType();
+ boolean stream = mediaPlayer.isStreaming();
+
+ if (info.playable != null) {
+ editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA,
+ info.playable.getPlayableType());
+ editor.putBoolean(
+ PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM,
+ stream);
+ editor.putBoolean(
+ PlaybackPreferences.PREF_CURRENT_EPISODE_IS_VIDEO,
+ mediaType == MediaType.VIDEO);
+ if (info.playable instanceof FeedMedia) {
+ FeedMedia fMedia = (FeedMedia) info.playable;
+ 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);
+ }
+ info.playable.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();
+ }
+
+ /**
+ * Send ACTION_PLAYER_STATUS_CHANGED without changing the status attribute.
+ */
+ private void postStatusUpdateIntent() {
+ sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED));
+ }
+
+ 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 PlaybackServiceMediaPlayer.PSMPInfo info) {
+ 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 (info.playable != null) {
+ int iconSize = getResources().getDimensionPixelSize(
+ android.R.dimen.notification_large_icon_width);
+ icon = BitmapDecoder
+ .decodeBitmapFromWorkerTaskResource(iconSize,
+ info.playable);
+ }
+
+ }
+ 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() && info.playerStatus == PlayerStatus.PLAYING
+ && info.playable != null) {
+ String contentText = info.playable.getFeedTitle();
+ String contentTitle = info.playable.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();
+ }
+
+ }
+
+ /**
+ * Saves the current position of the media file to the DB
+ */
+ private synchronized void saveCurrentPosition() {
+ int position = getCurrentPosition();
+ final Playable playable = mediaPlayer.getPSMPInfo().playable;
+ if (position != INVALID_TIME && playable != null) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Saving current position to " + position);
+ playable.saveCurrentPosition(PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext()),
+ position);
+ }
+ }
+
+ private void stopWidgetUpdater() {
+ taskManager.cancelWidgetUpdater();
+ sendBroadcast(new Intent(PlayerWidget.STOP_WIDGET_UPDATE));
+ }
+
+ private void updateWidget() {
+ PlaybackService.this.sendBroadcast(new Intent(
+ PlayerWidget.FORCE_WIDGET_UPDATE));
+ }
+
+ public boolean sleepTimerActive() {
+ return taskManager.isSleepTimerActive();
+ }
+
+ public long getSleepTimerTimeLeft() {
+ return taskManager.getSleepTimerTimeLeft();
+ }
+
+ @SuppressLint("NewApi")
+ private RemoteControlClient setupRemoteControlClient() {
+ if (Build.VERSION.SDK_INT < 14) {
+ return null;
+ }
+
+ Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+ mediaButtonIntent.setComponent(new ComponentName(getPackageName(),
+ MediaButtonReceiver.class.getName()));
+ 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(PlaybackServiceMediaPlayer.PSMPInfo info) {
+ if (android.os.Build.VERSION.SDK_INT >= 14) {
+ if (remoteControlClient != null) {
+ switch (info.playerStatus) {
+ 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 (info.playable != null) {
+ MetadataEditor editor = remoteControlClient
+ .editMetadata(false);
+ editor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE,
+ info.playable.getEpisodeTitle());
+
+ editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM,
+ info.playable.getFeedTitle());
+
+ editor.apply();
+ }
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "RemoteControlClient state was refreshed");
+ }
+ }
+ }
+
+ private void bluetoothNotifyChange(PlaybackServiceMediaPlayer.PSMPInfo info) {
+ boolean isPlaying = false;
+
+ if (info.playerStatus == PlayerStatus.PLAYING) {
+ isPlaying = true;
+ }
+
+ if (info.playable != null) {
+ Intent i = new Intent(AVRCP_ACTION_PLAYER_STATUS_CHANGED);
+ i.putExtra("id", 1);
+ i.putExtra("artist", "");
+ i.putExtra("album", info.playable.getFeedTitle());
+ i.putExtra("track", info.playable.getEpisodeTitle());
+ i.putExtra("playing", isPlaying);
+ final List<FeedItem> queue = taskManager.getQueueIfLoaded();
+ if (queue != null) {
+ i.putExtra("ListSize", queue.size());
+ }
+ i.putExtra("duration", info.playable.getDuration());
+ i.putExtra("position", info.playable.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() != null &&
+ 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) {
+ 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()) {
+ mediaPlayer.pause(true, true);
+ }
+ }
+
+ private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction() != null &&
+ intent.getAction().equals(ACTION_SHUTDOWN_PLAYBACK_SERVICE)) {
+ stopSelf();
+ }
+ }
+
+ };
+
+ private BroadcastReceiver skipCurrentEpisodeReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction() != null &&
+ intent.getAction().equals(ACTION_SKIP_CURRENT_EPISODE)) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent");
+ mediaPlayer.endPlayback();
+ }
+ }
+ };
+
+ public static MediaType getCurrentMediaType() {
+ return currentMediaType;
+ }
+
+ public void resume() {
+ mediaPlayer.resume();
+ }
+
+ public void prepare() {
+ mediaPlayer.prepare();
+ }
+
+ public void pause(boolean abandonAudioFocus, boolean reinit) {
+ mediaPlayer.pause(abandonAudioFocus, reinit);
+ }
+
+ public void reinit() {
+ mediaPlayer.reinit();
+ }
+
+ public PlaybackServiceMediaPlayer.PSMPInfo getPSMPInfo() {
+ return mediaPlayer.getPSMPInfo();
+ }
+
+ public PlayerStatus getStatus() {
+ return mediaPlayer.getPSMPInfo().playerStatus;
+ }
+
+ public Playable getPlayable() {
+ return mediaPlayer.getPSMPInfo().playable;
+ }
+
+ public void setSpeed(float speed) {
+ mediaPlayer.setSpeed(speed);
+ }
+
+ public boolean canSetSpeed() {
+ return mediaPlayer.canSetSpeed();
+ }
+
+ public float getCurrentPlaybackSpeed() {
+ return mediaPlayer.getPlaybackSpeed();
+ }
+
+ public boolean isStartWhenPrepared() {
+ return mediaPlayer.isStartWhenPrepared();
+ }
+
+ public void setStartWhenPrepared(boolean s) {
+ mediaPlayer.setStartWhenPrepared(s);
+ }
+
+
+ public void seekTo(final int t) {
+ mediaPlayer.seekTo(t);
+ }
+
+
+ public void seekDelta(final int d) {
+ mediaPlayer.seekDelta(d);
+ }
+
+ /**
+ * @see de.danoeh.antennapod.service.playback.PlaybackServiceMediaPlayer#seekToChapter(de.danoeh.antennapod.feed.Chapter)
+ */
+ public void seekToChapter(Chapter c) {
+ mediaPlayer.seekToChapter(c);
+ }
+
+ /**
+ * call getDuration() on mediaplayer or return INVALID_TIME if player is in
+ * an invalid state.
+ */
+ public int getDuration() {
+ return mediaPlayer.getDuration();
+ }
+
+ /**
+ * call getCurrentPosition() on mediaplayer or return INVALID_TIME if player
+ * is in an invalid state.
+ */
+ public int getCurrentPosition() {
+ return mediaPlayer.getPosition();
+ }
+
+ public boolean isStreaming() {
+ return mediaPlayer.isStreaming();
+ }
+
+ public Pair<Integer, Integer> getVideoSize() {
+ return mediaPlayer.getVideoSize();
+ }
+
+ private void setCurrentlyPlayingMedia(long id) {
+ SharedPreferences.Editor editor = PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext()).edit();
+ editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, id);
+ editor.commit();
+ }
+}
diff --git a/src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java b/src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java
new file mode 100644
index 000000000..52cde2718
--- /dev/null
+++ b/src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java
@@ -0,0 +1,917 @@
+package de.danoeh.antennapod.service.playback;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.RemoteControlClient;
+import android.util.Log;
+import android.util.Pair;
+import android.view.SurfaceHolder;
+import de.danoeh.antennapod.AppConfig;
+import de.danoeh.antennapod.feed.Chapter;
+import de.danoeh.antennapod.feed.MediaType;
+import de.danoeh.antennapod.preferences.UserPreferences;
+import de.danoeh.antennapod.receiver.MediaButtonReceiver;
+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.VideoPlayer;
+
+import java.io.IOException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Manages the MediaPlayer object of the PlaybackService.
+ */
+public class PlaybackServiceMediaPlayer {
+ public static final String TAG = "PlaybackServiceMediaPlayer";
+
+ /**
+ * Return value of some PSMP methods if the method call failed.
+ */
+ public static final int INVALID_TIME = -1;
+
+ private final AudioManager audioManager;
+
+ private volatile PlayerStatus playerStatus;
+ private volatile PlayerStatus statusBeforeSeeking;
+ private volatile IPlayer mediaPlayer;
+ private volatile Playable media;
+
+ private volatile boolean stream;
+ private volatile MediaType mediaType;
+ private volatile AtomicBoolean startWhenPrepared;
+ private volatile boolean pausedBecauseOfTransientAudiofocusLoss;
+ private volatile Pair<Integer, Integer> videoSize;
+
+ /**
+ * Some asynchronous calls might change the state of the MediaPlayer object. Therefore calls in other threads
+ * have to wait until these operations have finished.
+ */
+ private final ReentrantLock playerLock;
+
+ private final PSMPCallback callback;
+ private final Context context;
+
+ private final ExecutorService executor;
+
+ public PlaybackServiceMediaPlayer(Context context, PSMPCallback callback) {
+ if (context == null)
+ throw new IllegalArgumentException("context = null");
+ if (callback == null)
+ throw new IllegalArgumentException("callback = null");
+
+ this.context = context;
+ this.callback = callback;
+ this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ this.playerLock = new ReentrantLock();
+ this.startWhenPrepared = new AtomicBoolean(false);
+ executor = Executors.newSingleThreadExecutor();
+ mediaPlayer = null;
+ statusBeforeSeeking = null;
+ pausedBecauseOfTransientAudiofocusLoss = false;
+ mediaType = MediaType.UNKNOWN;
+ playerStatus = PlayerStatus.STOPPED;
+ videoSize = null;
+ }
+
+ /**
+ * Starts or prepares playback of the specified Playable object. If another Playable object is already being played, the currently playing
+ * episode will be stopped and replaced with the new Playable object. If the Playable object is already being played, the method will
+ * not do anything.
+ * Whether playback starts immediately depends on the given parameters. See below for more details.
+ * <p/>
+ * States:
+ * During execution of the method, the object will be in the INITIALIZING state. The end state depends on the given parameters.
+ * <p/>
+ * If 'prepareImmediately' is set to true, the method will go into PREPARING state and after that into PREPARED state. If
+ * 'startWhenPrepared' is set to true, the method will additionally go into PLAYING state.
+ * <p/>
+ * If an unexpected error occurs while loading the Playable's metadata or while setting the MediaPlayers data source, the object
+ * will enter the ERROR state.
+ * <p/>
+ * This method is executed on an internal executor service.
+ *
+ * @param playable The Playable object that is supposed to be played. This parameter must not be null.
+ * @param stream The type of playback. If false, the Playable object MUST provide access to a locally available file via
+ * getLocalMediaUrl. If true, the Playable object MUST provide access to a resource that can be streamed by
+ * the Android MediaPlayer via getStreamUrl.
+ * @param startWhenPrepared Sets the 'startWhenPrepared' flag. This flag determines whether playback will start immediately after the
+ * episode has been prepared for playback. Setting this flag to true does NOT mean that the episode will be prepared
+ * for playback immediately (see 'prepareImmediately' parameter for more details)
+ * @param prepareImmediately Set to true if the method should also prepare the episode for playback.
+ */
+ public void playMediaObject(final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
+ if (playable == null)
+ throw new IllegalArgumentException("playable = null");
+ if (AppConfig.DEBUG) Log.d(TAG, "Play media object.");
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+ try {
+ playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately);
+ } catch (RuntimeException e) {
+ throw e;
+ } finally {
+ playerLock.unlock();
+ }
+ }
+ });
+ }
+
+ /**
+ * Internal implementation of playMediaObject. This method has an additional parameter that allows the caller to force a media player reset even if
+ * the given playable parameter is the same object as the currently playing media.
+ * <p/>
+ * This method requires the playerLock and is executed on the caller's thread.
+ *
+ * @see #playMediaObject(de.danoeh.antennapod.util.playback.Playable, boolean, boolean, boolean)
+ */
+ private void playMediaObject(final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
+ if (playable == null)
+ throw new IllegalArgumentException("playable = null");
+ if (!playerLock.isHeldByCurrentThread())
+ throw new IllegalStateException("method requires playerLock");
+
+
+ if (media != null) {
+ if (!forceReset && media.getIdentifier().equals(playable.getIdentifier())) {
+ // episode is already playing -> ignore method call
+ return;
+ } else {
+ // stop playback of this episode
+ if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PREPARED) {
+ mediaPlayer.stop();
+ }
+ setPlayerStatus(PlayerStatus.INDETERMINATE, null);
+ }
+ }
+
+ this.media = playable;
+ this.stream = stream;
+ this.mediaType = media.getMediaType();
+ this.videoSize = null;
+ createMediaPlayer();
+ PlaybackServiceMediaPlayer.this.startWhenPrepared.set(startWhenPrepared);
+ setPlayerStatus(PlayerStatus.INITIALIZING, media);
+ try {
+ media.loadMetadata();
+ if (stream) {
+ mediaPlayer.setDataSource(media.getStreamUrl());
+ } else {
+ mediaPlayer.setDataSource(media.getLocalMediaUrl());
+ }
+ setPlayerStatus(PlayerStatus.INITIALIZED, media);
+
+ if (mediaType == MediaType.VIDEO) {
+ VideoPlayer vp = (VideoPlayer) mediaPlayer;
+ // vp.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT);
+ }
+
+ if (prepareImmediately) {
+ setPlayerStatus(PlayerStatus.PREPARING, media);
+ mediaPlayer.prepare();
+ onPrepared(startWhenPrepared);
+ }
+
+ } catch (Playable.PlayableException e) {
+ e.printStackTrace();
+ setPlayerStatus(PlayerStatus.ERROR, null);
+ } catch (IOException e) {
+ e.printStackTrace();
+ setPlayerStatus(PlayerStatus.ERROR, null);
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ setPlayerStatus(PlayerStatus.ERROR, null);
+ }
+ }
+
+
+ /**
+ * Resumes playback if the PSMP object is in PREPARED or PAUSED state. If the PSMP object is in an invalid state.
+ * nothing will happen.
+ * <p/>
+ * This method is executed on an internal executor service.
+ */
+ public void resume() {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+ resumeSync();
+ playerLock.unlock();
+ }
+ });
+ }
+
+ private void resumeSync() {
+ if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) {
+ int focusGained = audioManager.requestAudioFocus(
+ audioFocusChangeListener, AudioManager.STREAM_MUSIC,
+ AudioManager.AUDIOFOCUS_GAIN);
+ if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+
+ setSpeed(Float.parseFloat(UserPreferences.getPlaybackSpeed()));
+ mediaPlayer.start();
+ if (playerStatus == PlayerStatus.PREPARED && media.getPosition() > 0) {
+ mediaPlayer.seekTo(media.getPosition());
+ }
+
+ setPlayerStatus(PlayerStatus.PLAYING, media);
+ pausedBecauseOfTransientAudiofocusLoss = false;
+ if (android.os.Build.VERSION.SDK_INT >= 14) {
+ RemoteControlClient remoteControlClient = callback.getRemoteControlClient();
+ if (remoteControlClient != null) {
+ audioManager
+ .registerRemoteControlClient(remoteControlClient);
+ }
+ }
+ audioManager
+ .registerMediaButtonEventReceiver(new ComponentName(context.getPackageName(),
+ MediaButtonReceiver.class.getName()));
+ media.onPlaybackStart();
+
+ } else {
+ if (AppConfig.DEBUG) Log.e(TAG, "Failed to request audio focus");
+ }
+ } else {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Call to resume() was ignored because current state of PSMP object is " + playerStatus);
+ }
+ }
+
+
+ /**
+ * Saves the current position and pauses playback. Note that, if audiofocus
+ * is abandoned, the lockscreen controls will also disapear.
+ * <p/>
+ * This method is executed on an internal executor service.
+ *
+ * @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(final boolean abandonFocus, final boolean reinit) {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+
+ if (playerStatus == PlayerStatus.PLAYING) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Pausing playback.");
+ mediaPlayer.pause();
+ setPlayerStatus(PlayerStatus.PAUSED, media);
+
+ if (abandonFocus) {
+ audioManager.abandonAudioFocus(audioFocusChangeListener);
+ pausedBecauseOfTransientAudiofocusLoss = false;
+ }
+ if (stream && reinit) {
+ reinit();
+ }
+ } else {
+ if (AppConfig.DEBUG) Log.d(TAG, "Ignoring call to pause: Player is in " + playerStatus + " state");
+ }
+
+ playerLock.unlock();
+ }
+ });
+ }
+
+ /**
+ * Prepared media player for playback if the service is in the INITALIZED
+ * state.
+ * <p/>
+ * This method is executed on an internal executor service.
+ */
+ public void prepare() {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+
+ if (playerStatus == PlayerStatus.INITIALIZED) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Preparing media player");
+ setPlayerStatus(PlayerStatus.PREPARING, media);
+ try {
+ mediaPlayer.prepare();
+ onPrepared(startWhenPrepared.get());
+ } catch (IOException e) {
+ e.printStackTrace();
+ setPlayerStatus(PlayerStatus.ERROR, null);
+ }
+ }
+ playerLock.unlock();
+
+ }
+ });
+ }
+
+ /**
+ * Called after media player has been prepared. This method is executed on the caller's thread.
+ */
+ void onPrepared(final boolean startWhenPrepared) {
+ playerLock.lock();
+
+ if (playerStatus != PlayerStatus.PREPARING) {
+ playerLock.unlock();
+ throw new IllegalStateException("Player is not in PREPARING state");
+ }
+
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Resource prepared");
+
+ if (mediaType == MediaType.VIDEO) {
+ VideoPlayer vp = (VideoPlayer) mediaPlayer;
+ videoSize = new Pair<Integer, Integer>(vp.getVideoWidth(), vp.getVideoHeight());
+ }
+
+ if (media.getPosition() > 0) {
+ mediaPlayer.seekTo(media.getPosition());
+ }
+
+ if (media.getDuration() == 0) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Setting duration of media");
+ media.setDuration(mediaPlayer.getDuration());
+ }
+ setPlayerStatus(PlayerStatus.PREPARED, media);
+
+ if (startWhenPrepared) {
+ resumeSync();
+ }
+
+ playerLock.unlock();
+ }
+
+ /**
+ * Resets the media player and moves it into INITIALIZED state.
+ * <p/>
+ * This method is executed on an internal executor service.
+ */
+ public void reinit() {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+
+ if (media != null) {
+ playMediaObject(media, true, stream, startWhenPrepared.get(), false);
+ } else if (mediaPlayer != null) {
+ mediaPlayer.reset();
+ } else {
+ if (AppConfig.DEBUG) Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null");
+ }
+ playerLock.unlock();
+ }
+ });
+ }
+
+
+ /**
+ * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing.
+ * Invalid time values (< 0) will be ignored.
+ * <p/>
+ * This method is executed on the caller's thread.
+ */
+ private void seekToSync(int t) {
+ if (t < 0) {
+ if (AppConfig.DEBUG) Log.d(TAG, "Received invalid value for t");
+ return;
+ }
+ playerLock.lock();
+
+ if (playerStatus == PlayerStatus.PLAYING
+ || playerStatus == PlayerStatus.PAUSED
+ || playerStatus == PlayerStatus.PREPARED) {
+ if (stream) {
+ // statusBeforeSeeking = playerStatus;
+ // setPlayerStatus(PlayerStatus.SEEKING, media);
+ }
+ mediaPlayer.seekTo(t);
+
+ } else if (playerStatus == PlayerStatus.INITIALIZED) {
+ media.setPosition(t);
+ startWhenPrepared.set(true);
+ prepare();
+ }
+ playerLock.unlock();
+ }
+
+ /**
+ * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing.
+ * Invalid time values (< 0) will be ignored.
+ * <p/>
+ * This method is executed on an internal executor service.
+ */
+ public void seekTo(final int t) {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ seekToSync(t);
+ }
+ });
+ }
+
+ /**
+ * Seek a specific position from the current position
+ *
+ * @param d offset from current position (positive or negative)
+ */
+ public void seekDelta(final int d) {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+ int currentPosition = getPosition();
+ if (currentPosition != INVALID_TIME) {
+ seekToSync(currentPosition + d);
+ } else {
+ Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta");
+ }
+
+ playerLock.unlock();
+ }
+ });
+ }
+
+ /**
+ * Seek to the start of the specified chapter.
+ */
+ public void seekToChapter(Chapter c) {
+ if (c == null)
+ throw new IllegalArgumentException("c = null");
+ seekTo((int) c.getStart());
+ }
+
+ /**
+ * Returns the duration of the current media object or INVALID_TIME if the duration could not be retrieved.
+ */
+ public int getDuration() {
+ if (!playerLock.tryLock()) {
+ return INVALID_TIME;
+ }
+
+ int retVal = INVALID_TIME;
+ if (playerStatus == PlayerStatus.PLAYING
+ || playerStatus == PlayerStatus.PAUSED
+ || playerStatus == PlayerStatus.PREPARED) {
+ retVal = mediaPlayer.getDuration();
+ } else if (media != null && media.getDuration() > 0) {
+ retVal = media.getDuration();
+ }
+
+ playerLock.unlock();
+ return retVal;
+ }
+
+ /**
+ * Returns the position of the current media object or INVALID_TIME if the position could not be retrieved.
+ */
+ public int getPosition() {
+ if (!playerLock.tryLock()) {
+ return INVALID_TIME;
+ }
+
+ int retVal = INVALID_TIME;
+ if (playerStatus == PlayerStatus.PLAYING
+ || playerStatus == PlayerStatus.PAUSED
+ || playerStatus == PlayerStatus.PREPARED) {
+ retVal = mediaPlayer.getCurrentPosition();
+ } else if (media != null && media.getPosition() > 0) {
+ retVal = media.getPosition();
+ }
+
+ playerLock.unlock();
+ return retVal;
+ }
+
+ public boolean isStartWhenPrepared() {
+ return startWhenPrepared.get();
+ }
+
+ public void setStartWhenPrepared(boolean startWhenPrepared) {
+ this.startWhenPrepared.set(startWhenPrepared);
+ }
+
+ /**
+ * Returns true if the playback speed can be adjusted. This method can also return false if the PSMP object's
+ * internal MediaPlayer cannot be accessed at the moment.
+ */
+ public boolean canSetSpeed() {
+ if (!playerLock.tryLock()) {
+ return false;
+ }
+ boolean retVal = false;
+ if (mediaPlayer != null && media != null && media.getMediaType() == MediaType.AUDIO) {
+ retVal = (mediaPlayer).canSetSpeed();
+ }
+
+ playerLock.unlock();
+ return retVal;
+ }
+
+ /**
+ * Sets the playback speed.
+ * This method is executed on the caller's thread.
+ */
+ private void setSpeedSync(float speed) {
+ playerLock.lock();
+ if (media != null && media.getMediaType() == MediaType.AUDIO) {
+ if (mediaPlayer.canSetSpeed()) {
+ mediaPlayer.setPlaybackSpeed((float) speed);
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Playback speed was set to " + speed);
+ callback.playbackSpeedChanged(speed);
+ }
+ }
+ playerLock.unlock();
+ }
+
+ /**
+ * Sets the playback speed.
+ * This method is executed on an internal executor service.
+ */
+ public void setSpeed(final float speed) {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ setSpeedSync(speed);
+ }
+ });
+ }
+
+ /**
+ * Returns the current playback speed. If the playback speed could not be retrieved, 1 is returned.
+ */
+ public float getPlaybackSpeed() {
+ if (!playerLock.tryLock()) {
+ return 1;
+ }
+
+ int retVal = 1;
+ if (playerStatus == PlayerStatus.PLAYING
+ || playerStatus == PlayerStatus.PAUSED
+ || playerStatus == PlayerStatus.PREPARED) {
+ retVal = mediaPlayer.getCurrentPosition();
+ } else if (media != null && media.getPosition() > 0) {
+ retVal = media.getPosition();
+ }
+
+ playerLock.unlock();
+ return retVal;
+ }
+
+ public MediaType getCurrentMediaType() {
+ return mediaType;
+ }
+
+ public boolean isStreaming() {
+ return stream;
+ }
+
+
+ /**
+ * Releases internally used resources. This method should only be called when the object is not used anymore.
+ */
+ public void shutdown() {
+ executor.shutdown();
+ if (mediaPlayer != null) {
+ mediaPlayer.release();
+ }
+ }
+
+ public void setVideoSurface(final SurfaceHolder surface) {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+ if (mediaPlayer != null) {
+ mediaPlayer.setDisplay(surface);
+ }
+ playerLock.unlock();
+ }
+ });
+ }
+
+ public void resetVideoSurface() {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Resetting video surface");
+ mediaPlayer.setDisplay(null);
+ reinit();
+ playerLock.unlock();
+ }
+ });
+ }
+
+ /**
+ * Return width and height of the currently playing video as a pair.
+ *
+ * @return Width and height as a Pair or null if the video size could not be determined. The method might still
+ * return an invalid non-null value if the getVideoWidth() and getVideoHeight() methods of the media player return
+ * invalid values.
+ */
+ public Pair<Integer, Integer> getVideoSize() {
+ if (!playerLock.tryLock()) {
+ // use cached value if lock can't be aquired
+ return videoSize;
+ }
+ Pair<Integer, Integer> res;
+ if (mediaPlayer == null || playerStatus == PlayerStatus.ERROR || mediaType != MediaType.VIDEO) {
+ res = null;
+ } else {
+ VideoPlayer vp = (VideoPlayer) mediaPlayer;
+ videoSize = new Pair<Integer, Integer>(vp.getVideoWidth(), vp.getVideoHeight());
+ res = videoSize;
+ }
+ playerLock.unlock();
+ return res;
+ }
+
+ /**
+ * Returns a PSMInfo object that contains information about the current state of the PSMP object.
+ *
+ * @return The PSMPInfo object.
+ */
+ public synchronized PSMPInfo getPSMPInfo() {
+ return new PSMPInfo(playerStatus, media);
+ }
+
+ /**
+ * Sets the player status of the PSMP object. PlayerStatus and media attributes have to be set at the same time
+ * so that getPSMPInfo can't return an invalid state (e.g. status is PLAYING, but media is null).
+ * <p/>
+ * This method will notify the callback about the change of the player status (even if the new status is the same
+ * as the old one).
+ *
+ * @param newStatus The new PlayerStatus. This must not be null.
+ * @param newMedia The new playable object of the PSMP object. This can be null.
+ */
+ private synchronized void setPlayerStatus(PlayerStatus newStatus, Playable newMedia) {
+ if (newStatus == null)
+ throw new IllegalArgumentException("newStatus = null");
+ if (AppConfig.DEBUG) Log.d(TAG, "Setting player status to " + newStatus);
+
+ this.playerStatus = newStatus;
+ this.media = newMedia;
+ callback.statusChanged(new PSMPInfo(playerStatus, media));
+ }
+
+ private IPlayer createMediaPlayer() {
+ if (mediaPlayer != null) {
+ mediaPlayer.release();
+ }
+ if (media == null || media.getMediaType() == MediaType.VIDEO) {
+ mediaPlayer = new VideoPlayer();
+ } else {
+ mediaPlayer = new AudioPlayer(context);
+ }
+ mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
+ return setMediaPlayerListeners(mediaPlayer);
+ }
+
+ private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
+
+ @Override
+ public void onAudioFocusChange(final int focusChange) {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+
+ switch (focusChange) {
+ case AudioManager.AUDIOFOCUS_LOSS:
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Lost audio focus");
+ pause(true, false);
+ callback.shouldStop();
+ break;
+ case AudioManager.AUDIOFOCUS_GAIN:
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Gained audio focus");
+ if (pausedBecauseOfTransientAudiofocusLoss) // we paused => play now
+ resume();
+ else // we ducked => raise audio level back
+ audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
+ AudioManager.ADJUST_RAISE, 0);
+ break;
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+ if (playerStatus == 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 = false;
+ } 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 (playerStatus == PlayerStatus.PLAYING) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Lost audio focus temporarily. Pausing...");
+ pause(false, false);
+ pausedBecauseOfTransientAudiofocusLoss = true;
+ }
+ }
+
+ playerLock.unlock();
+ }
+ });
+
+ }
+ };
+
+ public void endPlayback() {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+
+ if (playerStatus != PlayerStatus.INDETERMINATE) {
+ setPlayerStatus(PlayerStatus.INDETERMINATE, media);
+ }
+ if (mediaPlayer != null) {
+ mediaPlayer.reset();
+
+ }
+ callback.endPlayback(true);
+
+ playerLock.unlock();
+ }
+ });
+ }
+
+ /**
+ * Holds information about a PSMP object.
+ */
+ public class PSMPInfo {
+ public PlayerStatus playerStatus;
+ public Playable playable;
+
+ public PSMPInfo(PlayerStatus playerStatus, Playable playable) {
+ this.playerStatus = playerStatus;
+ this.playable = playable;
+ }
+ }
+
+ public static interface PSMPCallback {
+ public void statusChanged(PSMPInfo newInfo);
+
+ public void shouldStop();
+
+ public void playbackSpeedChanged(float s);
+
+ public void onBufferingUpdate(int percent);
+
+ public boolean onMediaPlayerInfo(int code);
+
+ public boolean onMediaPlayerError(Object inObj, int what, int extra);
+
+ public boolean endPlayback(boolean playNextEpisode);
+
+ public RemoteControlClient getRemoteControlClient();
+ }
+
+ private IPlayer setMediaPlayerListeners(IPlayer mp) {
+ if (mp != null && media != null) {
+ if (media.getMediaType() == MediaType.AUDIO) {
+ ((AudioPlayer) mp)
+ .setOnCompletionListener(audioCompletionListener);
+ ((AudioPlayer) mp)
+ .setOnSeekCompleteListener(audioSeekCompleteListener);
+ ((AudioPlayer) mp).setOnErrorListener(audioErrorListener);
+ ((AudioPlayer) mp)
+ .setOnBufferingUpdateListener(audioBufferingUpdateListener);
+ ((AudioPlayer) mp).setOnInfoListener(audioInfoListener);
+ } else {
+ ((VideoPlayer) mp)
+ .setOnCompletionListener(videoCompletionListener);
+ ((VideoPlayer) mp)
+ .setOnSeekCompleteListener(videoSeekCompleteListener);
+ ((VideoPlayer) mp).setOnErrorListener(videoErrorListener);
+ ((VideoPlayer) mp)
+ .setOnBufferingUpdateListener(videoBufferingUpdateListener);
+ ((VideoPlayer) mp).setOnInfoListener(videoInfoListener);
+ }
+ }
+ return mp;
+ }
+
+ 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();
+ }
+
+ 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) {
+ callback.onBufferingUpdate(percent);
+ }
+
+ 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) {
+ return callback.onMediaPlayerInfo(what);
+ }
+
+ 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) {
+ return callback.onMediaPlayerError(inObj, what, extra);
+ }
+
+ 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() {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+ if (playerStatus == PlayerStatus.SEEKING) {
+ setPlayerStatus(statusBeforeSeeking, media);
+ }
+ playerLock.unlock();
+ }
+ });
+ }
+}
diff --git a/src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java b/src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java
new file mode 100644
index 000000000..4060ab041
--- /dev/null
+++ b/src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java
@@ -0,0 +1,384 @@
+package de.danoeh.antennapod.service.playback;
+
+import android.content.Context;
+import android.util.Log;
+import de.danoeh.antennapod.AppConfig;
+import de.danoeh.antennapod.feed.EventDistributor;
+import de.danoeh.antennapod.feed.FeedItem;
+import de.danoeh.antennapod.storage.DBReader;
+import de.danoeh.antennapod.util.playback.Playable;
+
+import java.util.List;
+import java.util.concurrent.*;
+
+/**
+ * Manages the background tasks of PlaybackSerivce, i.e.
+ * the sleep timer, the position saver, the widget updater and
+ * the queue loader.
+ * <p/>
+ * The PlaybackServiceTaskManager(PSTM) uses a callback object (PSTMCallback)
+ * to notify the PlaybackService about updates from the running tasks.
+ */
+public class PlaybackServiceTaskManager {
+ private static final String TAG = "PlaybackServiceTaskManager";
+
+ /**
+ * Update interval of position saver in milliseconds.
+ */
+ public static final int POSITION_SAVER_WAITING_INTERVAL = 5000;
+ /**
+ * Notification interval of widget updater in milliseconds.
+ */
+ public static final int WIDGET_UPDATER_NOTIFICATION_INTERVAL = 1500;
+
+ private static final int SCHED_EX_POOL_SIZE = 2;
+ private final ScheduledThreadPoolExecutor schedExecutor;
+
+ private ScheduledFuture positionSaverFuture;
+ private ScheduledFuture widgetUpdaterFuture;
+ private ScheduledFuture sleepTimerFuture;
+ private volatile Future<List<FeedItem>> queueFuture;
+ private volatile Future chapterLoaderFuture;
+
+ private SleepTimer sleepTimer;
+
+ private final Context context;
+ private final PSTMCallback callback;
+
+ /**
+ * Sets up a new PSTM. This method will also start the queue loader task.
+ *
+ * @param context
+ * @param callback A PSTMCallback object for notifying the user about updates. Must not be null.
+ */
+ public PlaybackServiceTaskManager(Context context, PSTMCallback callback) {
+ if (context == null)
+ throw new IllegalArgumentException("context must not be null");
+ if (callback == null)
+ throw new IllegalArgumentException("callback must not be null");
+
+ this.context = context;
+ this.callback = callback;
+ 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;
+ }
+ });
+ loadQueue();
+ EventDistributor.getInstance().register(eventDistributorListener);
+ }
+
+ private final EventDistributor.EventListener eventDistributorListener = new EventDistributor.EventListener() {
+ @Override
+ public void update(EventDistributor eventDistributor, Integer arg) {
+ if ((EventDistributor.QUEUE_UPDATE & arg) != 0) {
+ cancelQueueLoader();
+ loadQueue();
+ }
+ }
+ };
+
+ private synchronized boolean isQueueLoaderActive() {
+ return queueFuture != null && !queueFuture.isDone();
+ }
+
+ private synchronized void cancelQueueLoader() {
+ if (isQueueLoaderActive()) {
+ queueFuture.cancel(true);
+ }
+ }
+
+ private synchronized void loadQueue() {
+ if (!isQueueLoaderActive()) {
+ queueFuture = schedExecutor.submit(new Callable<List<FeedItem>>() {
+ @Override
+ public List<FeedItem> call() throws Exception {
+ return DBReader.getQueue(context);
+ }
+ });
+ }
+ }
+
+ /**
+ * Returns the queue if it is already loaded or null if it hasn't been loaded yet.
+ * In order to wait until the queue has been loaded, use getQueue()
+ */
+ public synchronized List<FeedItem> getQueueIfLoaded() {
+ if (queueFuture.isDone()) {
+ try {
+ return queueFuture.get();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } catch (ExecutionException e) {
+ e.printStackTrace();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the queue or waits until the PSTM has loaded the queue from the database.
+ */
+ public synchronized List<FeedItem> getQueue() throws InterruptedException {
+ try {
+ return queueFuture.get();
+ } catch (ExecutionException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Starts the position saver task. If the position saver is already active, nothing will happen.
+ */
+ public synchronized void startPositionSaver() {
+ if (!isPositionSaverActive()) {
+ Runnable positionSaver = new Runnable() {
+ @Override
+ public void run() {
+ callback.positionSaverTick();
+ }
+ };
+ positionSaverFuture = schedExecutor.scheduleWithFixedDelay(positionSaver, POSITION_SAVER_WAITING_INTERVAL,
+ POSITION_SAVER_WAITING_INTERVAL, TimeUnit.MILLISECONDS);
+
+ if (AppConfig.DEBUG) Log.d(TAG, "Started PositionSaver");
+ } else {
+ if (AppConfig.DEBUG) Log.d(TAG, "Call to startPositionSaver was ignored.");
+ }
+ }
+
+ /**
+ * Returns true if the position saver is currently running.
+ */
+ public synchronized boolean isPositionSaverActive() {
+ return positionSaverFuture != null && !positionSaverFuture.isCancelled() && !positionSaverFuture.isDone();
+ }
+
+ /**
+ * Cancels the position saver. If the position saver is not running, nothing will happen.
+ */
+ public synchronized void cancelPositionSaver() {
+ if (isPositionSaverActive()) {
+ positionSaverFuture.cancel(false);
+ if (AppConfig.DEBUG) Log.d(TAG, "Cancelled PositionSaver");
+ }
+ }
+
+ /**
+ * Starts the widget updater task. If the widget updater is already active, nothing will happen.
+ */
+ public synchronized void startWidgetUpdater() {
+ if (!isWidgetUpdaterActive()) {
+ Runnable widgetUpdater = new Runnable() {
+ @Override
+ public void run() {
+ callback.onWidgetUpdaterTick();
+ }
+ };
+ widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(widgetUpdater, WIDGET_UPDATER_NOTIFICATION_INTERVAL,
+ WIDGET_UPDATER_NOTIFICATION_INTERVAL, TimeUnit.MILLISECONDS);
+
+ if (AppConfig.DEBUG) Log.d(TAG, "Started WidgetUpdater");
+ } else {
+ if (AppConfig.DEBUG) Log.d(TAG, "Call to startWidgetUpdater was ignored.");
+ }
+ }
+
+ /**
+ * Starts a new sleep timer with the given waiting time. If another sleep timer is already active, it will be
+ * cancelled first.
+ * After waitingTime has elapsed, onSleepTimerExpired() will be called.
+ *
+ * @throws java.lang.IllegalArgumentException if waitingTime <= 0
+ */
+ public synchronized void setSleepTimer(long waitingTime) {
+ if (waitingTime <= 0)
+ throw new IllegalArgumentException("waitingTime <= 0");
+
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime)
+ + " milliseconds");
+ if (isSleepTimerActive()) {
+ sleepTimerFuture.cancel(true);
+ }
+ sleepTimer = new SleepTimer(waitingTime);
+ sleepTimerFuture = schedExecutor.schedule(sleepTimer, 0, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * Returns true if the sleep timer is currently active.
+ */
+ public synchronized boolean isSleepTimerActive() {
+ return sleepTimer != null && sleepTimerFuture != null && !sleepTimerFuture.isCancelled() && !sleepTimerFuture.isDone();
+ }
+
+ /**
+ * Disables the sleep timer. If the sleep timer is not active, nothing will happen.
+ */
+ public synchronized void disableSleepTimer() {
+ if (isSleepTimerActive()) {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Disabling sleep timer");
+ sleepTimerFuture.cancel(true);
+ }
+ }
+
+ /**
+ * Returns the current sleep timer time or 0 if the sleep timer is not active.
+ */
+ public synchronized long getSleepTimerTimeLeft() {
+ if (isSleepTimerActive()) {
+ return sleepTimer.getWaitingTime();
+ } else {
+ return 0;
+ }
+ }
+
+
+ /**
+ * Returns true if the widget updater is currently running.
+ */
+ public synchronized boolean isWidgetUpdaterActive() {
+ return widgetUpdaterFuture != null && !widgetUpdaterFuture.isCancelled() && !widgetUpdaterFuture.isDone();
+ }
+
+ /**
+ * Cancels the widget updater. If the widget updater is not running, nothing will happen.
+ */
+ public synchronized void cancelWidgetUpdater() {
+ if (isWidgetUpdaterActive()) {
+ widgetUpdaterFuture.cancel(false);
+ if (AppConfig.DEBUG) Log.d(TAG, "Cancelled WidgetUpdater");
+ }
+ }
+
+ private synchronized void cancelChapterLoader() {
+ if (isChapterLoaderActive()) {
+ chapterLoaderFuture.cancel(true);
+ }
+ }
+
+ private synchronized boolean isChapterLoaderActive() {
+ return chapterLoaderFuture != null && !chapterLoaderFuture.isDone();
+ }
+
+ /**
+ * Starts a new thread that loads the chapter marks from a playable object. If another chapter loader is already active,
+ * it will be cancelled first.
+ * On completion, the callback's onChapterLoaded method will be called.
+ */
+ public synchronized void startChapterLoader(final Playable media) {
+ if (media == null)
+ throw new IllegalArgumentException("media = null");
+
+ if (isChapterLoaderActive()) {
+ cancelChapterLoader();
+ }
+
+ Runnable chapterLoader = new Runnable() {
+ @Override
+ public void run() {
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Chapter loader started");
+ if (media.getChapters() == null) {
+ media.loadChapterMarks();
+ if (!Thread.currentThread().isInterrupted() && media.getChapters() != null) {
+ callback.onChapterLoaded(media);
+ }
+ }
+ if (AppConfig.DEBUG)
+ Log.d(TAG, "Chapter loader stopped");
+ }
+ };
+ chapterLoaderFuture = schedExecutor.submit(chapterLoader);
+ }
+
+
+ /**
+ * Cancels all tasks. The PSTM will be in the initial state after execution of this method.
+ */
+ public synchronized void cancelAllTasks() {
+ cancelPositionSaver();
+ cancelWidgetUpdater();
+ disableSleepTimer();
+ cancelQueueLoader();
+ cancelChapterLoader();
+ }
+
+ /**
+ * Cancels all tasks and shuts down the internal executor service of the PSTM. The object should not be used after
+ * execution of this method.
+ */
+ public synchronized void shutdown() {
+ EventDistributor.getInstance().unregister(eventDistributorListener);
+ cancelAllTasks();
+ schedExecutor.shutdown();
+ }
+
+ /**
+ * Sleeps for a given time and then pauses playback.
+ */
+ private class SleepTimer implements Runnable {
+ private static final String TAG = "SleepTimer";
+ private static final long UPDATE_INTERVALL = 1000L;
+ private volatile long waitingTime;
+ private volatile boolean isWaiting;
+
+ public SleepTimer(long waitingTime) {
+ super();
+ this.waitingTime = waitingTime;
+ isWaiting = true;
+ }
+
+ @Override
+ public void run() {
+ 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 (!Thread.currentThread().isInterrupted()) {
+ callback.onSleepTimerExpired();
+ }
+ postExecute();
+ }
+ } catch (InterruptedException e) {
+ Log.d(TAG, "Thread was interrupted while waiting");
+ break;
+ }
+ }
+ postExecute();
+ }
+
+ protected void postExecute() {
+ isWaiting = false;
+ }
+
+ public long getWaitingTime() {
+ return waitingTime;
+ }
+
+ public boolean isWaiting() {
+ return isWaiting;
+ }
+
+ }
+
+ public static interface PSTMCallback {
+ void positionSaverTick();
+
+ void onSleepTimerExpired();
+
+ void onWidgetUpdaterTick();
+
+ void onChapterLoaded(Playable media);
+ }
+}
diff --git a/src/de/danoeh/antennapod/service/PlayerStatus.java b/src/de/danoeh/antennapod/service/playback/PlayerStatus.java
index fbf5b1505..3d2b4ad39 100644
--- a/src/de/danoeh/antennapod/service/PlayerStatus.java
+++ b/src/de/danoeh/antennapod/service/playback/PlayerStatus.java
@@ -1,14 +1,14 @@
-package de.danoeh.antennapod.service;
+package de.danoeh.antennapod.service.playback;
public enum PlayerStatus {
+ INDETERMINATE, // player is currently changing its state, listeners should wait until the player has left this state.
ERROR,
PREPARING,
PAUSED,
PLAYING,
STOPPED,
PREPARED,
- SEEKING,
- AWAITING_VIDEO_SURFACE, // player has been initialized and the media type to be played is a video.
+ SEEKING,
INITIALIZING, // playback service is loading the Playable's metadata
INITIALIZED // playback service was started, data source of media player was set.
}
diff --git a/src/de/danoeh/antennapod/service/PlayerWidgetService.java b/src/de/danoeh/antennapod/service/playback/PlayerWidgetService.java
index 475af9655..f4674d395 100644
--- a/src/de/danoeh/antennapod/service/PlayerWidgetService.java
+++ b/src/de/danoeh/antennapod/service/playback/PlayerWidgetService.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.service;
+package de.danoeh.antennapod.service.playback;
import android.app.PendingIntent;
import android.app.Service;
@@ -72,9 +72,11 @@ public class PlayerWidgetService extends Service {
}
private void updateViews() {
+ if (playbackService == null) {
+ return;
+ }
isUpdating = true;
- if (AppConfig.DEBUG)
- Log.d(TAG, "Updating widget views");
+
ComponentName playerWidget = new ComponentName(this, PlayerWidget.class);
AppWidgetManager manager = AppWidgetManager.getInstance(this);
RemoteViews views = new RemoteViews(getPackageName(),
@@ -83,8 +85,8 @@ public class PlayerWidgetService extends Service {
PlaybackService.getPlayerActivityIntent(this), 0);
views.setOnClickPendingIntent(R.id.layout_left, startMediaplayer);
- if (playbackService != null && playbackService.getMedia() != null) {
- Playable media = playbackService.getMedia();
+ final Playable media = playbackService.getPlayable();
+ if (playbackService != null && media != null) {
PlayerStatus status = playbackService.getStatus();
views.setTextViewText(R.id.txtvTitle, media.getEpisodeTitle());
@@ -101,8 +103,6 @@ public class PlayerWidgetService extends Service {
views.setOnClickPendingIntent(R.id.butPlay,
createMediaButtonIntent());
} else {
- if (AppConfig.DEBUG)
- Log.d(TAG, "No media playing. Displaying defaultt views");
views.setViewVisibility(R.id.txtvProgress, View.INVISIBLE);
views.setTextViewText(R.id.txtvTitle,
this.getString(R.string.no_media_playing_label));
@@ -126,8 +126,8 @@ public class PlayerWidgetService extends Service {
}
private String getProgressString(PlaybackService ps) {
- int position = ps.getCurrentPositionSafe();
- int duration = ps.getDurationSafe();
+ int position = ps.getCurrentPosition();
+ int duration = ps.getDuration();
if (position != PlaybackService.INVALID_TIME
&& duration != PlaybackService.INVALID_TIME) {
return Converter.getDurationStringLong(position) + " / "